SDK reference, Incode Web SDK 2 Reference / Web SDK 2 Individual Modules

Selfie Module

Note

This guide is specific to Web SDK 2.0. If you are still using 1.x, you can find documentation here. Contact your Incode Representative for upgrade information and check if you are a candidate for this upgrade.

Full rollout to all clients still TBD.

The Selfie module captures a user's face with ML-powered liveness detection to prevent spoofing.

This module follows the camera-capture pattern. See that page for the shared manager lifecycle, capture sub-states, and skeleton; the rest of this page covers Selfie-specific config, detection statuses, and methods.

Tag

<incode-selfie> is a standard Web Component. Importing the UI subpath registers the custom element; importing the CSS applies the module's styles.

import '@incodetech/web/selfie';
import '@incodetech/web/selfie/styles.css';

Properties

Set these as JavaScript properties on the element (not as HTML attributes):

Property Type Required Description
config SelfieConfig ❌ Configuration options (validation flags, modes)
onFinish () => void ❌ Called when capture completes successfully
onError (error: string) => void ❌ Called when an error occurs

WASM Requirements

The Selfie module uses WebAssembly for face detection and liveness analysis. Pre-warm WASM during setup() so models are ready before the user reaches the camera step:

await setup({
  apiURL: 'https://demo-api.incodesmile.com',
  token: 'your-session-token',
  wasm: { pipelines: ['selfie'] },
});

See WASM Configuration for self-hosted paths and the lower-level warmupWasm() API.

Usage

Vanilla HTML / TypeScript

<incode-selfie></incode-selfie>

<script type="module">
  import { setup } from '@incodetech/core';
  import '@incodetech/web/selfie';
  import '@incodetech/web/selfie/styles.css';

  await setup({
    apiURL: 'https://demo-api.incodesmile.com',
    token: 'your-session-token',
    wasm: { pipelines: ['selfie'] },
  });

  const selfie = document.querySelector('incode-selfie');
  selfie.onFinish = () => console.log('Selfie captured!');
  selfie.onError = (err) => console.error('Selfie error:', err);
</script>

React

React 18 or earlier: add the one-time JSX augmentation from Framework Integration → TypeScript: JSX support for incode-* tags. React 19+ doesn't need it, and can also use the simpler form from Framework Integration → React 19+ shortcut.

import { useEffect, useRef } from 'react';
import { setup } from '@incodetech/core';
import type { SelfieConfig } from '@incodetech/core/selfie';
import '@incodetech/web/selfie';
import '@incodetech/web/selfie/styles.css';

type SelfieElement = HTMLElement & {
  config?: SelfieConfig;
  onFinish: () => void;
  onError: (error: string) => void;
};

await setup({
  apiURL: 'https://demo-api.incodesmile.com',
  token: 'your-session-token',
  wasm: { pipelines: ['selfie'] },
});

export function SelfieCapture() {
  const ref = useRef<SelfieElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    el.onFinish = () => console.log('Selfie captured!');
    el.onError = (err) => console.error('Selfie error:', err);
  }, []);

  return <incode-selfie ref={ref} />;
}

For Angular (CUSTOM_ELEMENTS_SCHEMA) and Vue (compilerOptions.isCustomElement) setup, see Framework Integration.

Headless Mode

For complete UI control, use the createSelfieManager from @incodetech/core/selfie.

Quick Start

import { setup } from '@incodetech/core';
import { createSelfieManager } from '@incodetech/core/selfie';
import { resolveDashboardModuleConfig } from '@incodetech/core/flow';
import { warmupWasm } from '@incodetech/core/wasm';

await setup({
  apiURL: 'https://demo-api.incodesmile.com',
  token: 'your-session-token',
});

await warmupWasm({
  wasmPath: '/wasm/webLib.wasm',
  glueCodePath: '/wasm/webLib.js',
  modelsBasePath: '/wasm/models',
  pipelines: ['selfie'],
});

const config = await resolveDashboardModuleConfig({ moduleKey: 'SELFIE' });
const manager = createSelfieManager({ config });

manager.subscribe((state) => {
  console.log('Status:', state.status);

  if (state.status === 'capture') {
    console.log('Detection:', state.detectionStatus);
    console.log('Stream ready:', !!state.stream);
  }

  if (state.status === 'finished') {
    console.log('Selfie captured!', state.processResponse);
    manager.stop();
  }
});

manager.load();

The core factory requires a complete SelfieConfig. This example resolves it from the active dashboard Flow, which must contain a SELFIE module. Standalone UI components accept partial overrides; those are not a complete core-manager config.

State Machine Flow

flowchart LR
    idle -->|load| tutorial
    tutorial -->|nextStep| permissions
    permissions -->|granted| capture
    capture -->|upload done| processing
    processing -->|success| finished

States Reference

Status Description Key Properties
idle Initial state, waiting for load() –
loading Checking permissions (when no tutorial) –
tutorial Showing tutorial. Entered when showTutorial is true or ageAssurance is true — the age-assurance variant doubles as the compliance/privacy notice, so it is shown even when the tutorial is disabled ageAssurance (boolean; mirrors config.ageAssurance and selects the age-assurance copy variant)
manualUpload File upload alternative, enabled by manualUploadSelfieCapture phase, uploaded, canContinue, errorKey, attemptsRemaining
permissions Camera permission handling permissionStatus
capture Camera active, detecting face stream, captureStatus, detectionStatus, attemptsRemaining
processing Server-side processing of the captured selfie –
finished Capture complete processResponse?
closed User closed the flow –
error Fatal error occurred error

Manual upload

In 2.3.0, tutorial always includes showManualUploadLink: boolean. Render an upload action only when it is true, and call goToManualUpload() from that action.

Property Type Description
phase 'selecting' | 'uploading' File selection or upload in progress.
uploaded boolean Whether the backend accepted a file.
canContinue boolean Enable Continue only when true.
errorKey string | null Translation key for current feedback.
attemptsRemaining number Remaining upload attempts.

These properties exist in manualUpload. Add that status to exhaustive SelfieState switches. Typed tutorial fixtures must include showManualUploadLink.

Selfie's manualUploadSelectFile(file) takes one argument. ID Capture's method takes (side, file); do not share the ID call signature with Selfie.

Capture State Properties

When status === 'capture':

Property Type Description
stream CameraStream | undefined Camera stream for the video element; wait until it is available before attaching it.
captureStatus string 'initializing', 'detecting', 'capturing', 'uploading', 'uploadError', 'success'
detectionStatus DetectionStatus Face detection feedback (see below)
attemptsRemaining number Remaining capture attempts
uploadError string? Error message if upload failed
assistedOnboarding boolean Whether assisted onboarding mode was requested in config
usingBackCamera boolean Whether stream is really a back camera — mirror your <video> when false
onDeviceMode boolean When true, do not offer a manual capture button.
obfuscateWithAvatar boolean Whether a cosmetic concealment overlay is enabled.
avatarCanvas HTMLCanvasElement? Display-only canvas; unavailable until ready or when concealment is off.
avatarVariant 'avatar-3d' | 'avatar-2d' | 'privacy-lens' (optional) Derived display variant. Keep the face outline for Privacy Lens; hide it for full avatars. This is not a host-settable config field.

Note

processing is a separate top-level state (status === 'processing'), not a captureStatus value.

Removed: debugFrame (ImageData?) is gone from the selfie, authentication, and ID capture state types. It had already stopped carrying frames — the per-frame emission was dropped because storing a full-resolution frame roughly ten times a second re-rendered the capture screen and starved it on lower-tier devices. Reading state.debugFrame is now a type error rather than a silent undefined. If you relied on per-frame pipeline frames, contact Incode: an opt-in hook is the intended replacement.

These are read-only presentation fields. In error, error: string carries the message and moduleErrorCode?: string carries an optional code. Forward that code unchanged when reporting failure to a Flow or Workflow manager.

Detection Status Values

Status User Instruction
idle "Preparing camera..."
detecting "Detecting face..."
noFace "Position your face in the frame"
tooManyFaces "Only one face should be visible"
tooClose "Move back"
tooFar "Move closer"
blur "Hold still, image is blurry"
dark "Improve lighting conditions"
faceAngle "Face your camera directly"
headWear "Remove head coverings"
lenses "Remove glasses or lenses"
eyesClosed "Open your eyes"
faceMask "Remove face mask"
centerFace "Center your face"
getReady "Get ready..."
getReadyFinished "Hold still..."
capturing "Capturing photo..."
manualCapture "Tap to capture"
offline "No network connection"

Permission Status Values

When status === 'permissions':

permissionStatus Description
idle Ready to request permission
requesting Permission dialog shown
denied User denied camera access
learnMore Showing help screen

Handling cancellation (closed)

When the user dismisses the selfie flow either by calling manager.close() or by clicking the SDK's built-in close button, the manager transitions to closed. closed** is a final state**: the manager won't re-emit further updates from there, and reset() doesn't apply (it is only valid from error). Custom UIs need to decide what to do next.

The two common patterns:

Inside an orchestrated flow: call flowManager.completeModule() to skip the selfie step and let the orchestrator advance:

selfieManager.subscribe((state) => {
  if (state.status === 'closed') {
    // Optional: render a brief "Cancelled — continuing…" screen, then advance.
    setTimeout(() => flowManager.completeModule(), 800);
  }
});

Standalone (no orchestrator): there's nowhere to advance to, so navigate the user out of the selfie surface entirely:

selfieManager.subscribe((state) => {
  if (state.status === 'closed') {
    selfieManager.stop(); // tear down resources
    navigate('/onboarding/cancelled');
  }
});

Common UX choice: render a brief "Cancelled — continuing…" screen for ~800 ms before calling flowManager.completeModule(), so the cancellation isn't jarring.

API Methods

Method Description When to Use
load() Starts the selfie flow Always call first
goToManualUpload() Open file upload tutorial, when showManualUploadLink is true
manualUploadSelectFile(file) Select a selfie file manualUpload, phase === 'selecting'
manualUploadContinue() Continue after upload manualUpload, when canContinue is true
nextStep() Advances from tutorial to permissions When tutorial
requestPermission() Requests camera access When permissions.idle or permissions.learnMore
goToLearnMore() Shows permission help screen When permissions.idle
back() Goes back from learn more When permissions.learnMore
capture() Manual capture trigger In capture, when !onDeviceMode && detectionStatus === 'manualCapture'
retryCapture() Retry after upload error When captureStatus === 'uploadError'
close() Close the flow Anytime
reset() Reset to initial state In error; create a new manager after finished
stop() Cleanup resources When unmounting
getState() Returns current state Anytime
subscribe(callback) Subscribe to state changes Returns unsubscribe function

React Example

import { useState, useEffect, useRef } from 'react';
import { createSelfieManager, type SelfieState, type SelfieConfig } from '@incodetech/core/selfie';

function CustomSelfie({ config }: { config: SelfieConfig }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [manager] = useState(() =>
    createSelfieManager({ config }),
  );
  const [state, setState] = useState<SelfieState>({ status: 'idle' });

  useEffect(() => {
    const unsubscribe = manager.subscribe(setState);
    manager.load();
    return () => {
      unsubscribe();
      manager.stop();
    };
  }, [manager]);

  useEffect(() => {
    if (state.status === 'capture' && state.stream && videoRef.current) {
      videoRef.current.srcObject = state.stream;
    }
  }, [state]);

  switch (state.status) {
    case 'tutorial':
      return (
        <div>
          <h2>Take a Selfie</h2>
          <ul>
            <li>Ensure good lighting</li>
            <li>Remove glasses and hats</li>
            <li>Look directly at the camera</li>
          </ul>
          <button onClick={() => manager.nextStep()}>Continue</button>
          {state.showManualUploadLink && <button onClick={() => manager.goToManualUpload()}>Upload a photo</button>}
        </div>
      );

    case 'permissions':
      if (state.permissionStatus === 'denied') {
        return (
          <div>
            <p>Camera access is required.</p>
            <p>Please enable camera in your browser settings.</p>
          </div>
        );
      }
      return (
        <div>
          <p>We need camera access to take your selfie.</p>
          <button onClick={() => manager.requestPermission()}>
            Allow Camera
          </button>
        </div>
      );

    case 'capture':
      return (
        <div>
          <video ref={videoRef} autoPlay playsInline muted />
          <p>{getDetectionMessage(state.detectionStatus)}</p>
          {state.captureStatus === 'uploading' && <p>Uploading...</p>}
          {!state.onDeviceMode && state.detectionStatus === 'manualCapture' && (
            <button onClick={() => manager.capture()}>Take Photo</button>
          )}
          {state.captureStatus === 'uploadError' && (
            <div>
              <p className="error">{state.uploadError}</p>
              <button onClick={() => manager.retryCapture()}>Retry</button>
            </div>
          )}
        </div>
      );

    case 'processing':
      return <div>Processing...</div>;

    case 'manualUpload':
      return (
        <div>
          <input type="file" accept="image/*" disabled={state.phase === 'uploading'}
            onChange={(event) => {
              const file = event.currentTarget.files?.[0];
              if (file) manager.manualUploadSelectFile(file);
            }} />
          {state.errorKey && <p role="alert">{state.errorKey}</p>}
          <button disabled={!state.canContinue} onClick={() => manager.manualUploadContinue()}>Continue</button>
        </div>
      );

    case 'finished':
      return <div>✅ Selfie captured successfully!</div>;

    case 'error':
      return <div className="error">Error: {state.error}</div>;

    default:
      return <div>Loading...</div>;
  }
}

function getDetectionMessage(status: string): string {
  const messages: Record<string, string> = {
    idle: 'Preparing camera...',
    detecting: 'Detecting face...',
    noFace: 'Position your face in the frame',
    tooManyFaces: 'Only one face should be visible',
    tooFar: 'Move closer',
    tooClose: 'Move back',
    blur: 'Hold still, image is blurry',
    dark: 'Improve lighting conditions',
    faceAngle: 'Face your camera directly',
    centerFace: 'Center your face',
    lenses: 'Remove glasses or lenses',
    faceMask: 'Remove face mask',
    capturing: 'Capturing...',
    manualCapture: 'Ready - tap to capture',
  };
  return messages[status] || 'Detecting face...';
}

Capture-only flow

createSelfieCaptureOnlyManager exposes the same state machine and API surface as createSelfieManager, but bypasses Incode's /omni/add/face upload. Instead of submitting the captured frame and waiting on server-side processing, the manager invokes a customer-supplied onCapture(response) callback with the raw face image and reaches finished locally. Use it when you want to capture in the browser but route the bytes through your own pipeline.

The config exposes supported capture UX options plus a required onCapture callback. It omits assistedOnboarding, enableFaceRecording, and deepsightLiveness:

import { setup } from '@incodetech/core';
import { initializeSession } from '@incodetech/core/session';
import {
  createSelfieCaptureOnlyManager,
  type SelfieCaptureOnlyConfig,
  type FaceCaptureOnlyResponse,
} from '@incodetech/core/selfie';

await setup({
  apiURL: 'https://demo-api.incodesmile.com',
  wasm: { pipelines: ['selfie'] },
});
await initializeSession({ token: 'your-session-token' });

const config: SelfieCaptureOnlyConfig = {
  showTutorial: true,
  showPreview: false,
  autoCaptureTimeout: 10,
  captureAttempts: 3,
  validateLenses: true,
  validateFaceMask: true,
  validateHeadCover: true,
  validateClosedEyes: true,
  validateBrightness: true,
  onCapture: async (response: FaceCaptureOnlyResponse) => {
    const { image } = response;
    await uploadToMyBackend(image.blob);
  },
};

const manager = createSelfieCaptureOnlyManager({ config });
manager.subscribe((state) => {
  if (state.status === 'finished') manager.stop();
});
manager.load();

The FaceCaptureOnlyResponse payload is:

type FaceCaptureOnlyResponse = {
  image: FaceCapturedImageData;
};

type FaceCapturedImageData = {
  imageBase64: string; // The unprocessed full frame, base64-encoded
  blob: Blob; // Same content as Blob
  url: string; // Object-URL for direct rendering
  metadata: string; // Capture metadata (serialized)
  videoBase64: string | undefined; // Local-recording bytes — see note below
};

Warning

Sensitive biometric payload. Capture-only delivers unprocessed biometric data directly to your code. The standard selfie flow encrypts the upload payload at the transport layer; capture-only does not — the bytes leave the SDK as plain base64 / Blob and you are responsible for handling and transmitting them securely.

The standard capture-only factory does not enable recording and has no recording flag. Its shared response type includes videoBase64, but this path normally leaves it undefined; do not depend on a video artifact. Protect image data in transit and storage.

createSelfieCaptureOnlyManagerFromActor is also exported for advanced cases where you supply a pre-built XState actor.

Configuration Options

SelfieConfig is FlowModuleConfig['SELFIE'] & BaseFaceCaptureConfig. The FlowModuleConfig half is the dashboard-driven shape and its fields are required from your perspective; BaseFaceCaptureConfig adds a few optional UI overrides.

Orchestrated vs headless: when <incode-selfie> runs inside <incode-flow> (or createOrchestratedFlowManager), the orchestrator supplies the complete dashboard configuration. createSelfieManager({ config }) also requires a complete configuration. The standalone <incode-selfie>.config property accepts partial overrides and resolves remaining dashboard fields when configuration merging is enabled (the default).

Option Type Required Description
manualUploadSelfieCapture boolean ❌ Offer file upload from the tutorial when enabled. Default disabled.
showTutorial boolean ✅ Show tutorial before capture
showPreview boolean ✅ Show preview after capture
assistedOnboarding boolean ✅ Staff-assisted mode: use the back camera with no mirroring. Devices without a back camera keep the front camera and stay mirrored — read usingBackCamera from capture state to know which applies
enableFaceRecording boolean ✅ Enable client-side video recording streamed through multipart upload
autoCaptureTimeout number ✅ Seconds before auto-capture triggers
captureAttempts number ✅ Maximum capture attempts
validateLenses boolean ✅ Reject captures with glasses/lenses
validateFaceMask boolean ✅ Reject captures with face mask
validateHeadCover boolean ✅ Reject captures with head coverings
validateClosedEyes boolean ✅ Reject captures with eyes closed
validateBrightness boolean ✅ Reject captures with poor lighting
deepsightLiveness 'SINGLE_FRAME' | 'MULTIMODAL' | 'VIDEOLIVENESS' ✅ Liveness detection mode
numberOfAttempts number ❌ Legacy alias for captureAttempts. Prefer captureAttempts.
cameraResolution { width?: number; height?: number } ❌ Preferred camera resolution
ageAssurance boolean ❌ Show the age-assurance copy variant in the tutorial. Mirrors the flow-level age-assurance flag — when true, the tutorial state surfaces ageAssurance: true so the UI renders the alternate copy. Because this copy is the age-assurance privacy notice, the tutorial is shown even when showTutorial is false (matching the ID module's ageVerification behavior).
onDeviceFaceResultsSubmissionEnabled boolean ❌ Opt-in. When true, face analysis runs entirely on-device and only the results are submitted to the server. Has E2EE and WASM-pipeline prerequisites — see On-Device Face Capture for the full walkthrough. Leave off to keep the legacy server-side pipeline.
selfieConcealmentOption 'OPTION_NONE' | 'OPTION_SILHOUETTE' | 'OPTION_2D' | 'OPTION_3D' ❌ Conceals the on-screen camera preview with a cosmetic avatar. See Face concealment below. Default OPTION_NONE (live preview).
avatarAssets AvatarAssetsOverrides ❌ Where to load the avatar runtime from. Only read when selfieConcealmentOption selects an avatar. See Self-hosting the avatar runtime below.

Face concealment

Privacy Lens is the OPTION_SILHOUETTE concealment option. selfieConcealmentOption replaces the live camera preview with a cosmetic stand-in, for flows where users are reluctant to see themselves on screen:

Value On-screen preview
OPTION_NONE The live camera feed. This is the default.
OPTION_SILHOUETTE A frosted-glass treatment over the feed.
OPTION_2D A stylized 2D face.
OPTION_3D A 3D character.

The option is display-only. Face detection, quality checks, liveness, age estimation, and upload all continue to run on the real camera frames, so concealment never changes the verification result. An absent or unrecognized value renders the live preview.

Inside <incode-flow> or createOrchestratedFlowManager, the value comes from the SELFIE node configuration in your dashboard and the orchestrator passes it through — you do not set it yourself. Standalone integrators set the field on config directly.

Self-hosting the avatar runtime

The avatar options load their runtime — a face-tracking library and the avatar assets — from Incode's CDN. Deployments that block third-party origins can serve them from their own host with avatarAssets:

const config: Partial<SelfieConfig> = {
  selfieConcealmentOption: 'OPTION_2D',
  avatarAssets: { basePath: 'https://your-cdn.example.com/incode-avatar' },
};
Field Type Description
basePath string Serves every avatar asset from this directory instead of the default host.
version string Selects a specific asset release within basePath.

Per-asset path overrides also exist for deployments that cannot keep the expected directory layout. Ask your Incode account team for the asset manifest and required headers before mirroring — the file set differs per avatar option, and a partial mirror falls back to the live camera preview rather than failing loudly.

Omit avatarAssets and the SDK uses its defaults. The field is read only when selfieConcealmentOption selects an avatar, so it has no effect on OPTION_NONE.

Troubleshooting

Face Not Detected

  • Ensure good lighting (avoid backlighting)
  • Remove glasses or hats if possible
  • Position face within the outline
  • Check WASM is properly initialized

Camera Issues

Upload Errors

  • Check network connectivity
  • Verify session token is valid
  • Check WASM files are accessible

See Also

Was this page helpful?