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

ID Capture Module

Warning

This guide is specific to Web SDK 2.x. For Web SDK 1.x, you can find documentation here.

Do not attempt to upgrade or implement without support. Contact your Incode representative to check if you are a candidate for this upgrade.

The ID Capture module captures front and back images of identity documents (ID cards, passports) with ML-powered quality checks and validation. It also supports digital ID verification through supported device wallets and bank redirects. For implementation details, see Digital ID: mDL Verification and Digital ID: Bank Redirect.

This module follows the camera-capture pattern, plus dedicated states for mandatory consent and digital IDs. See the patterns page for the shared lifecycle; the rest of this page covers ID-specific config, capture properties, and methods.

Tag

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

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

Properties

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

Property Type Required Description
config IdCaptureConfig ❌ Configuration for ID capture behavior
manager IdCaptureManager ❌ Optional pre-built manager (advanced use)
onFinish () => void ❌ Called when capture completes successfully
onError (error: string | undefined) => void ❌ Called when an error occurs

WASM Requirements

The ID Capture module uses WebAssembly for document detection, blur/glare quality checks, and perspective correction. 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: ['idCapture'] },
});

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

Usage

Vanilla HTML / TypeScript

<incode-id></incode-id>

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

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

  const id = document.querySelector('incode-id');
  id.config = {
    showTutorial: true,
    enableId: true,
    enablePassport: false,
    autoCaptureTimeout: 5,
    captureAttempts: 3,
  };
  id.onFinish = () => console.log('ID captured!');
  id.onError = (err) => console.error('ID 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 { IdCaptureConfig } from '@incodetech/core/id';
import '@incodetech/web/id';
import '@incodetech/web/id/styles.css';

type IdElement = HTMLElement & {
  config: Partial<IdCaptureConfig>;
  onFinish: () => void;
  onError: (error: string | undefined) => void;
};

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

export function IdCapture() {
  const ref = useRef<IdElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    el.config = {
      showTutorial: true,
      enableId: true,
      enablePassport: false,
      autoCaptureTimeout: 5,
      captureAttempts: 3,
    };
    el.onFinish = () => console.log('ID captured!');
    el.onError = (err) => console.error('ID error:', err);
  }, []);

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

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

Workflow vs Flow

In a Dashboard-driven Flow (<incode-flow> / createOrchestratedFlowManager), ID is a single step that captures the document and runs server-side processing (/process/id). This is what <incode-id> and createIdCaptureManager do by default.

In a server-driven Workflow (<incode-workflow> / createWorkflowManager), the workflow engine emits ID handling as two distinct nodes:

  1. ID_CAPTURE: runs idCaptureMachine for the capture portion only, then advances without calling /process/id. Enabled by setting IdCaptureConfig.skipProcessId = true (the workflow engine sets this automatically on the ID_CAPTURE node's config).
  2. ID: runs the headless id-verification module (@incodetech/core/id-verification, createIdVerificationManager) which invokes /process/id on its own. The orchestrator renders the corresponding <incode-id-verification> shell while the request is in flight. No UI work needed.code

This split lets the Workflow inject custom logic (manual review, retries, branching) between capture and verification. For a single-step capture+verify flow, keep using the standard <incode-id> / createIdCaptureManager path; skipProcessId defaults to false, so processing runs inline as before.

The id-verification module ships:

  • createIdVerificationManager from @incodetech/core/id-verification: headless manager for the verification half (used by Workflow's ID node, also usable headlessly outside Workflow).
  • IdVerificationConfig: config type for the manager.
  • <incode-id-verification>: Preact UI shell registered automatically by the orchestrator; no public @incodetech/web/id-verification import path, since the orchestrator owns the mount.

Standalone ID verification

After SDK setup and session activation, createIdVerificationManager({ config }) can process an ID already captured in that session. An empty config uses the first ID.

IdVerificationConfig option Type Meaning
isSecondId boolean? Set true to process the second captured ID; default false.
idRank 'FIRST_ID' | 'SECOND_ID' | 'THIRD_ID' (deprecated) Workflow-provided rank. SECOND_ID also selects the second ID. THIRD_ID is deprecated and does not enable third-ID processing.
queueName string? Processing queue name; defaults to an empty string.

Either isSecondId: true or idRank: 'SECOND_ID' selects the second ID; otherwise the first ID is processed.

Status Action
idle Call load() to begin processing.
processing Show progress while the request runs.
expired Show expiration feedback and call continue() on user confirmation.
finished Module complete; this does not establish verification approval.

The state variants carry only status. The manager also exposes subscribe(), getState(), and stop(). Unsubscribe and call stop() when your host tears down; create a new manager for a new run.

import { createIdVerificationManager } from '@incodetech/core/id-verification';

// After setup and activation of the session containing the captured ID:
const verification = createIdVerificationManager({ config: {} });
const unsubscribe = verification.subscribe((state) => {
  if (state.status === 'expired') {
    // Show expiration feedback; call verification.continue() on confirmation.
  }
});
verification.load();

function disposeVerification() {
  unsubscribe();
  verification.stop();
}

Headless Mode

For complete UI control, use the createIdCaptureManager from @incodetech/core/id.

Quick Start

import { setup } from '@incodetech/core';
import { createIdCaptureManager } from '@incodetech/core/id';
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: ['idCapture'],
});

const config = await resolveDashboardModuleConfig({ moduleKey: 'ID' });
const manager = createIdCaptureManager({ config });

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

  if (state.status === 'capture') {
    console.log('Mode:', state.currentMode); // 'front' or 'back'
    console.log('Detection:', state.detectionStatus);
    console.log('Counter:', state.counterValue); // Auto-capture countdown
  }

  if (state.status === 'finished') {
    console.log('ID captured successfully!');
    manager.stop();
  }
});

manager.load();

The core factory requires a complete IdCaptureConfig. Resolve it from an active Dashboard Flow containing ID, or supply all required fields. UI components accept partial overrides; core factories do not fill them automatically.

State Machine Flow

flowchart LR
    idle -->|load| chooser
    chooser -->|selectDocument| tutorial
    tutorial -->|continueToPermission| permissions
    permissions -->|granted| capture
    capture -->|front done| frontFinished
    frontFinished --> capture
    capture -->|back done| processing
    processing --> finished

States Reference

Status Description Key Properties
idle Initial state, waiting for load() –
chooser Document type selection. digitalIdRedirectOptions lists one entry per enabled digital-ID bank-redirect provider (iDIN, FTN, …); render each as its own option. availableDocumentTypes, digitalIdRedirectOptions
loading Loading configuration or a transient permission/camera phase phase, tutorialEnabled, selectedDocumentType, currentMode, requiresMotionPermissionUserGesture
tutorial Showing tutorial phase, selectedDocumentType, currentMode, requiresMotionPermissionUserGesture
ageVerification Regulation-required age-confirmation gate before capture. Reached when config.ageAssurance === true. Advance with nextStep(). –
permissions Camera permission handling permissionStatus, requiresMotionPermissionUserGesture
capture Active document capture stream, currentMode, detectionStatus, counterValue, attemptsRemaining
frontFinished Front side complete, transitioning to back –
backFinished Back side complete –
mandatoryConsent Regulation-required consent screen regulationType (see Mandatory Consent)
processing Server-side document processing –
expiredExhausted Expired-document attempts exhausted; await Continue –
expired Document expired after processing –
digitalIdUpload Digital ID upload sub-flow — PDF e-IDs uploaded by file picker (see Digital ID Upload) full shape below
digitalIdRedirect Digital ID bank-redirect sub-flow — iDIN / FTN full-page redirect and resume (see Digital ID — Bank Redirect) phase, launchUrl, failureReason, providerId
finished Capture complete –
error Fatal error occurred error
closed User closed the flow –

Loading, tutorial, and permission properties

Property Type Available in
phase 'bootstrapping' | 'permissionsIdle' | 'checkingStream' | 'initializingDeepsight' | 'initializingCamera' loading
phase 'checkingPermission' | 'ready' | 'initializingCamera' | 'waitingForPermission' | 'waitingForCamera' tutorial
selectedDocumentType IdDocumentType | undefined loading, tutorial, permissions
currentMode 'front' | 'back' loading, tutorial, permissions
tutorialEnabled boolean loading, permissions
permissionStatus PermissionStatus permissions
requiresMotionPermissionUserGesture boolean loading, tutorial, permissions; start unresolved iOS motion permission from a user gesture with continueToPermission()

Keep the document and side visible while waiting. permissionsIdle is a silent permission check; avoid flashing a new full-screen loader. Tutorial ready waits for Continue. waitingForPermission and waitingForCamera mean Continue was already requested; show a waiting state. Treat the other loading phases as preparation without exposing implementation details to the user.

When status === 'tutorial', call continueToPermission() directly from the tutorial CTA's tap handler. This preserves the user activation required by the iOS DeviceMotion prompt. The phase is checkingPermission, ready, initializingCamera, waitingForPermission, or waitingForCamera. The state also exposes selectedDocumentType, currentMode, and requiresMotionPermissionUserGesture.

When status === 'loading', the state exposes phase, tutorialEnabled, selectedDocumentType, currentMode, and requiresMotionPermissionUserGesture. The phase is bootstrapping, permissionsIdle, checkingStream, initializingDeepsight, or initializingCamera. Loading is transient. Use the motion flag to keep custom presentation consistent, not to dispatch another permission action.

Capture State Properties

When status === 'capture':

Property Type Description
captureFeedbackMode 'countdown' | 'optimizing' Which capture-feedback presentation to render.
acceptedDocuments string[]? Accepted document names returned with ID_TYPE_UNACCEPTABLE. Entries are strings; preserve unknown names. Hide the list when absent or empty.
countryCode string? Country associated with the upload response, when available.
stream MediaStream Camera stream for <video> element
currentMode 'front' | 'back' Which side is being captured
captureStatus string 'initializing', 'detecting', 'capturing', 'optimizing', 'uploading', 'uploadError', 'success'. optimizing appears only when you opt in — see Capture result preview
detectionStatus string Document detection feedback
counterValue number Auto-capture countdown (seconds)
countdownStepMs number Milliseconds between counterValue steps (3 → 2 → 1 → 0). Lets you animate the whole countdown from its first value
attemptsRemaining number Remaining capture attempts
uploadError string? Error code if upload failed
uploadErrorMessage string? Human-readable error
needsBackCapture boolean Whether back capture is needed
usingBackCamera boolean Whether the active stream uses a rear camera. Mirror a front-camera preview; leave the rear-camera preview unmirrored.
barcodeFastPathCaptured boolean Render barcode-capture success without a captured-image preview when true.
needsFrontCapture boolean A front-side capture is required after the back. Use continueToFront() when the completed capture awaits that follow-up.
idType string | undefined Detected document type, when available.
canRetry boolean Whether retry is available
previewImageUrl string? Captured image preview URL
candidateImageUrl string? Best frame found so far, during optimizing only. Display-only — see Capture result preview
optimizingDwellMs number? How long the SDK holds the preview. A minimum, not an exact duration — see Capture result preview

When status === 'mandatoryConsent':

Property Type Description
regulationType RegulationTypes Which regional regulation triggered the consent screen. Used to pick consent copy.

RegulationTypes is a string union:

type RegulationTypes =
  | 'US'
  | 'Worldwide'
  | 'Other'
  | 'US_Illinois'
  | 'US_Texas'
  | 'US_California'
  | 'US_Washington';

'Other' is the fallback the SDK uses when the upload response doesn't pin a specific regulation. Drive your consent copy off this value.

Drive the screen with acceptMandatoryConsent() (proceed) or cancelMandatoryConsent() (abort the flow).

Digital ID Upload State Properties

When status === 'digitalIdUpload'. Reached when the backend allows uploading a digital ID (typically an EU eID or similar government-issued PDF) instead of capturing a physical document. The sub-flow walks the user through a tutorial → file picker → preview → upload → success/failure cycle with a bounded retry budget (3 attempts).

Property Type Description
phase union 'tutorial', 'selecting', 'reviewing', 'uploading', 'holding', 'success', 'error', 'fileTooLarge', 'exhausted'. 'holding' is a brief stabilization step shown while the upload finishes; treat as 'uploading' UI-wise.
file File | null The PDF the user picked (null outside 'reviewing' / 'uploading' / 'holding').
fileName string | undefined The file's display name. Use this to render the preview row.
failReason DigitalUploadFailReason | null Set when phase === 'error'. Union: 'DIGITAL_ID_REQUESTED_BUT_OTHER_PROVIDED', 'ID_TYPE_UNACCEPTABLE', 'FILE_CHANGED_ERROR', 'INVALID_FILE_TYPE', 'NETWORK_ERROR', 'GENERIC'.
attemptsRemaining number Remaining upload retries (starts at 3). Hits 0 → phase === 'exhausted'.
uploadProgress number Upload progress 0–100. Only meaningful in 'uploading' / 'holding'.
pickerRequestId number Monotonic counter — increments each time the SDK wants the host UI to open the file picker. Watch for changes and trigger your <input type="file"> programmatically (the SDK never opens it itself).

Accepted files. PDF only (application/pdf), 5 MB maximum.

Phase progression. tutorial → selecting (host opens picker on each pickerRequestId change) → reviewing (user confirms or replaces) → uploading → success (auto-advances out of the sub-flow) or error → back to selecting, until attemptsRemaining === 0 → exhausted. fileTooLarge is shown immediately when the user picks an oversized file and does not consume a retry.

Drive the screen with digitalUploadNextStep(), digitalUploadPickFile(file), digitalUploadConfirm(), digitalUploadReplace(), digitalUploadRetry(), digitalUploadChooseAnother(), and digitalUploadScanInstead() (falls back to live capture when allowed).

Digital ID Redirect State Properties

Digital methods must be enabled in Dashboard and supported by the current device. Render options from chooser.availableDocumentTypes; do not offer an unavailable wallet route.

Status Property Type / behavior
digitalIdRedirect phase 'loading' | 'redirecting' | 'success' | 'failed' | 'error'
digitalIdRedirect launchUrl string | undefined; navigate only when phase === 'redirecting' and a URL is present.
digitalIdRedirect failureReason, providerId Optional public failure/provider identifiers. See Bank Redirect.

Device Wallet State Properties

When status === 'deviceWallet'. Reached when the user picks the wallet tile (selectDocument('digitalId')). The SDK fetches a wallet request, opens the OS wallet sheet, and forwards the encrypted response to Incode. See Digital ID: mDL Verification for prerequisites, supported wallets, and the standalone renderWallet() API.

Property Type Description
phase 'requesting' | 'presenting' | 'decrypting' | 'failed' 'requesting' and 'decrypting' are loading steps. 'presenting' means the OS wallet sheet is open. 'failed' is recoverable.
failReason DeviceWalletFailReason | null Set when phase === 'failed': 'UNSUPPORTED_PLATFORM', 'NO_CREDENTIAL', or 'WALLET_ERROR'.
platform 'ios' | 'android' | undefined Wallet backend: 'ios' for Apple Wallet, 'android' for Google Wallet.

User activation. Call selectDocument('digitalId') inside your tap handler. Browsers open the wallet sheet only during the tap's user activation.

Outcome. A completed handshake moves to finished with no document upload — a 'REJECTED' decision finishes too. Verified attributes, including the mDL portrait, land on the session, so Face Match still runs downstream. On 'failed', call deviceWalletRetry() from a new tap, or deviceWalletUseAnotherMethod() to return to the chooser (or reach closed when there is no chooser).

Expired-document recovery

expired exposes attemptsRemaining and currentMode. Once attempts are exhausted, render expiredExhausted with a Continue action calling continueExhausted(). The configured journey determines whether the SDK captures another side, completes processing, or finishes this module. Continue does not establish an approved verification; use the session result.

Detection Status Values

Status User Instruction
idle "Preparing camera..."
idNotDetected "Position your ID in the frame"
detecting "Hold steady..."
farAway "Move closer"
blur "Hold still, image is blurry"
glare "Adjust angle to reduce glare"
wrongSide "Please show the other side"
capturing "Capturing..."
manualCapture "Tap to capture"
offline "No network connection"

API Methods

For an ID tutorial CTA, call continueToPermission() directly from the tap handler. You can call it unconditionally: it requests iOS motion only when requiresMotionPermissionUserGesture is true, then enters camera permission orchestration. nextStep() retains the legacy tutorial behavior, including motion priming when camera permission is already granted. Do not defer a gesture-dependent call through an effect or timer.

Method Description When to Use
continueExhausted() Continue after expired-document attempts are exhausted expiredExhausted
digitalIdRedirectRetry() Restart redirect verification digitalIdRedirect, failed or error phase
digitalIdRedirectUseAnotherMethod() Leave the redirect and use configured fallback digitalIdRedirect, failed or error phase
digitalIdRedirectContinue() Acknowledge the redirect success screen digitalIdRedirect, success phase
digitalIdRedirectRefresh() Refresh the pending launch URL digitalIdRedirect, redirecting phase
load() Starts the ID capture flow Always call first
selectDocument(type, options?) Selects a type from availableDocumentTypes ('id', 'passport', 'digitalId', …). For 'digitalIdRedirect', pass { providerId } from digitalIdRedirectOptions to launch that provider; omitted, the first configured method is used. When chooser. Call it inside the tap handler for 'digitalId'
nextStep() Advances through the legacy tutorial path When tutorial
requestPermission(options?) Requests camera access. Pass { trigger: 'automatic' } when code, not a user action, initiates it, so it is not reported as a click When permissions.idle
continueToPermission() Starts gesture-aware permission entry and requests unresolved iOS motion permission first Directly from the ID tutorial CTA
goToLearnMore() Shows permission help When permissions.idle
back() Goes back from learn more When permissions.learnMore
capture() Manual capture trigger When detectionStatus === 'manualCapture'
switchToManualCapture() Switch from auto to manual mode During auto-capture
switchToAutoCapture() Restart automatic capture on the same side without consuming a retry When detectionStatus === 'manualCapture', before taking the photo
openHelp() Pause detection and keep the camera stream open; returns true if Help opened, or false if capture has advanced During live automatic or manual capture; display Help only when this returns true
closeHelp() Resume capture in the current mode; automatic capture receives a fresh timeout After openHelp(), when Help is dismissed
retryCapture() Retry after upload error When canRetry is true
continueFromError() Continue after non-fatal error When error allows continuation
continueToBack() Proceed to back capture When frontFinished
continueToFront() Flip back to front capture When backFinished
skipBack() Skip back capture When back is optional
acceptMandatoryConsent() Accept the regulation-required consent When mandatoryConsent
cancelMandatoryConsent() Decline the regulation-required consent When mandatoryConsent
digitalUploadNextStep() Advance from the digital-upload tutorial to the picker When digitalIdUpload and phase === 'tutorial'
digitalUploadPickFile(file) Hand a picked PDF to the SDK for review When digitalIdUpload and phase === 'selecting'
digitalUploadConfirm() Confirm the previewed file and start uploading When digitalIdUpload and phase === 'reviewing'
digitalUploadReplace() Discard the previewed file and re-open the picker When digitalIdUpload and phase === 'reviewing'
digitalUploadRetry() Retry after an upload error (consumes one of attemptsRemaining) When digitalIdUpload and phase === 'error'
digitalUploadChooseAnother() Pick a different file after fileTooLarge When digitalIdUpload and phase === 'fileTooLarge'
digitalUploadScanInstead() Abandon digital upload and fall back to live capture When digitalIdUpload (any phase, if allowed by config)
deviceWalletRetry() Restart a failed wallet handshake. Call it from a user gesture When deviceWallet and phase === 'failed'
deviceWalletUseAnotherMethod() Leave the wallet and return to the chooser (or close) When deviceWallet and phase === 'failed'
updateDetectionArea(area) Update detection bounds On resize/orientation change
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 {
  createIdCaptureManager,
  type IdCaptureState,
  type IdCaptureConfig,
} from '@incodetech/core/id';

function CustomIdCapture({ config }: { config: IdCaptureConfig }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [manager] = useState(() =>
    createIdCaptureManager({
      config,
    }),
  );
  const [state, setState] = useState<IdCaptureState>({ 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 'chooser':
      return (
        <div>
          <button onClick={() => manager.selectDocument('id')}>ID Card</button>
          <button onClick={() => manager.selectDocument('passport')}>
            Passport
          </button>
          {state.digitalIdRedirectOptions.map((option) => (
            <button
              key={option.providerId}
              onClick={() =>
                manager.selectDocument('digitalIdRedirect', {
                  providerId: option.providerId,
                })
              }
            >
              {option.displayName ?? option.providerId}
            </button>
          ))}
        </div>
      );

    case 'permissions':
      return (
        <div>
          <p>Camera access is required</p>
          <button onClick={() => manager.requestPermission()}>
            Allow Camera
          </button>
        </div>
      );

    case 'capture':
      return (
        <div>
          <video ref={videoRef} autoPlay playsInline muted />
          <p>Capturing: {state.currentMode} side</p>
          <p>{state.detectionStatus}</p>
          {state.captureStatus === 'detecting' && (
            <p>Auto-capture in: {state.counterValue}s</p>
          )}
          {state.detectionStatus === 'manualCapture' && (
            <button onClick={() => manager.capture()}>Capture</button>
          )}
          {state.captureStatus === 'uploadError' && state.canRetry && (
            <button onClick={() => manager.retryCapture()}>Retry</button>
          )}
        </div>
      );

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

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

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

Capture-only flow

createIdCaptureOnlyManager exposes the same state machine and API surface as createIdCaptureManager, but bypasses the Incode upload pipeline. Instead of submitting the captured frames to Incode and waiting on server-side processing, the manager invokes a customer-supplied onCapture(response) callback with the captured images and reaches finished locally. Use this when you want to capture in the browser but upload (or process) the bytes through your own pipeline.

The config contains supported optional capture UX fields plus a required onCapture callback. It is a subset of IdCaptureConfig, not the full Dashboard configuration:

import { setup } from '@incodetech/core';
import { initializeSession } from '@incodetech/core/session';
import {
  createIdCaptureOnlyManager,
  type IdCaptureOnlyConfig,
  type CaptureOnlyResponse,
} from '@incodetech/core/id';

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

const config: IdCaptureOnlyConfig = {
  showTutorial: true,
  enableId: true,
  enablePassport: false,
  autoCaptureTimeout: 5,
  captureAttempts: 3,
  onCapture: async (response: CaptureOnlyResponse) => {
    // response.frontImage: IdCapturedImageData | undefined (undefined if onlyBack)
    // response.backImage: IdCapturedImageData | undefined (undefined if onlyFront)
    await uploadToMyBackend(
      response.frontImage?.blob,
      response.backImage?.blob,
    );
  },
};

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

The CaptureOnlyResponse payload is:

// `frontImage` is undefined only for a back-only capture (`onlyBack: true`).
type CaptureOnlyResponse =
  | { frontImage: IdCapturedImageData; backImage: IdCapturedImageData | undefined }
  | { frontImage: undefined; backImage: IdCapturedImageData };

type IdCapturedImageData = {
  imageBase64: string; // Always present — the unprocessed full frame
  blob: Blob; // Same content as Blob
  url: string; // Object-URL for direct rendering
  metadata: string; // Capture metadata (serialized)
  croppedImage?: {
    // Best-effort document crop (see caveat below)
    imageBase64: string;
    blob: Blob;
    url: string;
  };
};

About croppedImage. This is a best-effort approximate crop produced by a bilinear quad mapping — not a true perspective/homography warp. It is populated only when on-device quad detection succeeds, and it will visibly diverge from the correct projective result for strongly tilted captures. Prefer imageBase64 (the unprocessed full frame) when fidelity matters; treat croppedImage as a preview hint.

createIdCaptureOnlyManagerFromActor is also exported for advanced cases where you supply a pre-built XState actor (e.g. to swap services in tests).

Configuration Options

IdCaptureConfig is TutorialIdConfig & { ...extras } — TutorialIdConfig is the Dashboard-driven shape (almost all fields are required), and the extras are advanced overrides.

Orchestrated vs headless: <incode-flow> and createOrchestratedFlowManager supply the complete Dashboard configuration. createIdCaptureManager({ config }) also requires a complete configuration. The standalone <incode-id>.config property accepts partial overrides and resolves remaining Dashboard fields when configuration merging is enabled (the default).

Document selection

Option Type Required Description
enableId boolean ✅ Enable ID-card capture
enablePassport boolean ✅ Enable passport capture
deviceWallet boolean ✅ Enable the device-wallet (mDL) flow — Apple Wallet (web) / Google Wallet. A wallet method enabled in digitalIds also turns it on. See Digital ID: mDL Verification
digitalIdsUpload boolean ✅ Allow uploading digital IDs as an alternative
showDocumentChooserScreen boolean ✅ Render the document-type chooser screen

Digital ID configuration

Option Type Required Description
digitalIds IdCaptureDigitalIdMethod[] No Resolved Dashboard methods. Redirect entries use REDIRECT or WEB_REDIRECT flow types. An absent or empty list offers no redirect option.
digitalIdReturnUrl string No Registered HTTPS return page for bank verification.
digitalIdReturn { returnParams: RedirectReturnParams } No Pass only when resuming the return leg.

Import these types from @incodetech/core/id. A method has required provider and flowType, plus optional method, country, and displayName. Use the Dashboard-resolved configuration for your provisioned methods. See Bank Redirect for return-route wiring.

Capture behavior

Option Type Required Description
showTutorial boolean ✅ Show tutorial before capture
onlyFront boolean ✅ Declares that the document is one-sided (like a passport). Forwarded to the front-upload endpoint as the onlyFront query param. Not a client-side override: the server weighs it against its own classification of the uploaded image and answers with skipBackIdCapture, so a genuinely two-sided document will still ask for the back — passports included, since they are not always single-sided. For guaranteed front-only capture, configure it on Dashboard. Deliberately not symmetric with onlyBack, which is honored client-side.
onlyBack boolean ✅ Capture the back side only. The document chooser is skipped and capture starts on the back side.
barcodeCapture boolean ✅ Use barcode scanning when applicable
fetchAdditionalPage boolean ✅ Capture an additional page after the main scan
autoCaptureTimeout number ✅ Seconds before auto-capture triggers
deviceIdleTimeout number ✅ Seconds of device idleness before timing out
captureAttempts number ✅ Maximum retry attempts
enableIdRecording boolean ✅ Enable client-side front/back video recording streamed through multipart upload
usSmartCapture boolean ✅ US-specific smart-capture mode
secondId boolean ✅ Capture a second ID document after the first
showCaptureButtonInAuto boolean ❌ Show a manual capture button during auto-capture
alwaysCaptureBackOfId boolean ❌ Always capture the back of the ID, even when the front response would normally skip it
extractIdFace boolean ❌ Set false to skip biometric face extraction from the ID: no biometric template is created for the session. Use it in jurisdictions with biometric-consent restrictions. Face match is then unavailable for that session. Omit or set true to keep the default behavior.

Per-country / per-document overrides

Option Type Required Description
perCountryPerDocOverrides object ✅ Nested map keyed by country code → document type → { onlyFront, onlyBack, fetchAdditionalPage }. Not read by the SDK — per-document capture policy is resolved server-side and returned as skipBackIdCapture on the upload response. Pass {}.

Extras (from IdCaptureConfig beyond TutorialIdConfig)

Option Type Required Description
ageAssurance boolean ❌ Enable age assurance features
mergeSessionRecordings boolean ❌ Merge per-step recordings into a single session-level recording
isDeepsightEnabled boolean ❌ Override Deepsight enablement (otherwise driven by the session)
supportsOptimizingStatus boolean ❌ Opt into the mid-capture result preview — see Capture result preview below.

Capture result preview

The built-in <incode-id> component reassures the user mid-capture: partway through the frame-selection window it flashes and freezes the best frame found so far under a "Successfully captured" heading, while the camera and frame analysis keep running. If the user keeps holding the document steady, the frame that actually uploads may be a better one than the one they were shown. Headless integrations skip that preview by default.

To render your own version of it, create the manager with supportsOptimizingStatus: true. captureStatus then reaches optimizing partway through capture, carrying the best-frame-so-far on candidateImageUrl, and leaves it on its own once the frame is committed — at which point previewImageUrl carries the frame that will upload.

Read state.candidateImageUrl ?? state.previewImageUrl, not candidateImageUrl alone: on manual capture (the user taps the button) the preview runs after the frame is committed, so only previewImageUrl is set and a host reading just candidateImageUrl renders nothing. See Headless Mode for the per-path table.

const manager = createIdCaptureManager({
  config: { ...idConfig, supportsOptimizingStatus: true },
});

manager.subscribe((state) => {
  if (state.status === 'capture' && state.captureStatus === 'optimizing') {
    // Covers both paths: automatic capture sets `candidateImageUrl`,
    // manual capture only `previewImageUrl`. On the automatic path the image is
    // display-only — detection is still running and may find a better frame.
    showPreview(state.candidateImageUrl ?? state.previewImageUrl);
  }
});

optimizingDwellMs reports how long the SDK holds the preview. Treat it as a minimum: the SDK runs the dwell on its own timer on both paths, and on automatic capture the state additionally waits for the frame-selection loop to commit a frame, which can take slightly longer. React to captureStatus leaving optimizing rather than assuming an exact duration.

Nothing waits on you: the SDK bounds the state itself, so there is no callback to fire. Your integration should still handle the direct capture-to-upload path, because the preview does not appear on every session. Pass false to opt out explicitly. See Headless Mode for the full walkthrough.

IdCaptureConfig also exposes a few advanced fields reserved for Incode-internal tooling. They aren't part of the public integration surface and aren't documented here.

Error Codes

Code Description User Action
UPLOAD_ERROR Upload failed Retry capture
CLASSIFICATION_FAILED Document not recognized Use clearer image
LOW_SHARPNESS Image too blurry Hold device steadier
GLARE_DETECTED Glare on document Adjust lighting
WRONG_DOCUMENT_SIDE Wrong side shown Flip document
ID_TYPE_UNACCEPTABLE Document not supported Use different ID
READABILITY_ISSUE Cannot read document fields Improve lighting or angle
RETRY_EXHAUSTED_CONTINUE_TO_BACK Front retries exhausted; proceeding to back Auto-advances
RETRY_EXHAUSTED_SKIP_BACK Back retries exhausted; skipping back Auto-advances
NO_MORE_TRIES Max attempts reached Flow ends
UNEXPECTED_ERROR Unexpected internal error Retry or contact support
NO_TOKEN Session token missing Re-initialize SDK
PERMISSION_DENIED Camera access denied Enable in settings
USER_CANCELLED User cancelled the flow Re-open module
SERVER_ERROR Server-side error Retry later

Examples

Each example shows a config object you can assign to <incode-id>.config (or pass through your framework's property binding). Plug it into the React or vanilla pattern shown in Usage.

ID Card Only

const config: Partial<IdCaptureConfig> = {
  enableId: true,
  enablePassport: false,
  showTutorial: true,
  autoCaptureTimeout: 5,
};

Passport Only

const config: Partial<IdCaptureConfig> = {
  enableId: false,
  enablePassport: true,
  showTutorial: false,
};

See Also

Was this page helpful?