ID Capture Module

The ID Capture module captures front and back images of identity documents (ID cards, passports) with ML-powered quality checks and validation.

📘

This guide is specific to Web SDK 2.0. If you are still using 1.x, you can find documentation here. We strongly recommend upgrading - contact your Incode Representative for upgrade information.

The ID Capture module captures front and back images of identity documents (ID cards, passports) with ML-powered quality checks and validation.

Follows the camera-capture pattern, plus dedicated states for mandatory consent and manual file upload. 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):

PropertyTypeRequiredDescription
configIdCaptureConfigConfiguration for ID capture behavior
managerIdCaptureManagerOptional pre-built manager (advanced use)
onFinish() => voidCalled when capture completes successfully
onError(error: string | undefined) => voidCalled 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: 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.

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.

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 { 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 manager = createIdCaptureManager({
  config: {
    showTutorial: true,
    enableId: true,
    enablePassport: false,
    autoCaptureTimeout: 5,
    captureAttempts: 3,
  },
});

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();

State Machine Flow

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

States Reference

StatusDescriptionKey Properties
idleInitial state, waiting for load()
chooserDocument type selectionavailableDocumentTypes
loadingLoading configuration
tutorialShowing tutorialselectedDocumentType, currentMode
ageVerificationRegulation-required age-confirmation gate before capture. Reached when config.ageAssurance === true. Advance with nextStep().
permissionsCamera permission handlingpermissionStatus
captureActive document capturestream, currentMode, detectionStatus, counterValue, attemptsRemaining
frontFinishedFront side complete, transitioning to back
backFinishedBack side complete
mandatoryConsentRegulation-required consent screenregulationType (see Mandatory Consent)
processingServer-side document processing
expiredDocument expired after processing
manualUploadManual file upload flow (see Manual Upload)full shape below
digitalIdUploadDigital ID upload sub-flow — PDF e-IDs uploaded by file picker (see Digital ID Upload)full shape below
finishedCapture complete
errorFatal error occurrederror
closedUser closed the flow

Capture State Properties

When status === 'capture':

PropertyTypeDescription
streamMediaStreamCamera stream for <video> element
currentMode'front' | 'back'Which side is being captured
captureStatusstring'initializing', 'detecting', 'capturing', 'uploading', 'uploadError', 'success'
detectionStatusstringDocument detection feedback
counterValuenumberAuto-capture countdown (seconds)
attemptsRemainingnumberRemaining capture attempts
uploadErrorstring?Error code if upload failed
uploadErrorMessagestring?Human-readable error
needsBackCapturebooleanWhether back capture is needed
canRetrybooleanWhether retry is available
previewImageUrlstring?Captured image preview URL

Mandatory Consent State Properties

When status === 'mandatoryConsent':

PropertyTypeDescription
regulationTypeRegulationTypesWhich 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).

Manual Upload State Properties

When status === 'manualUpload'. The user is uploading ID images by file picker — used as a fallback when live capture isn't possible, or when the backend forces it via configuration. The state has tab-aware UI hints (ID vs Passport), per-slot file metadata, and a continue gate.

PropertyTypeDescription
phase'selecting' | 'uploading' | 'exhausted''selecting' while the user picks files, 'uploading' during upload, 'exhausted' after retries are spent.
uploadingSide'front' | 'back' | 'passport' | undefinedWhich side is currently uploading; undefined outside phase === 'uploading'.
activeTab'id' | 'passport'Currently selected tab. Use manualUploadChangeTab() to switch.
showIdTabbooleanWhether the ID tab should be rendered (driven by config).
showPassportTabbooleanWhether the Passport tab should be rendered.
showBackSlotbooleanOn the ID tab, whether to render the back-of-ID slot. The server controls visibility per document — the back slot is hidden when the front upload response sets skipBackIdCapture (e.g. passports, single-sided IDs).
frontFileNamestring | undefinedFile name the user picked for the front of the ID, if any.
backFileNamestring | undefinedFile name for the back of the ID, if any.
passportFileNamestring | undefinedFile name for the passport upload, if any.
frontUploadedbooleanTrue once the front file has been accepted server-side.
backUploadedbooleanTrue once the back file has been accepted server-side.
passportUploadedbooleanTrue once the passport file has been accepted server-side.
canContinuebooleanWhether the Continue button should be enabled. Use this to gate manualUploadContinue().
retriesLeftnumberRemaining retries on the current tab.
errorKeystring | nulli18n key for the current error message. Look it up via your translation table; null when there is no error.

Drive the screen with manualUploadSelectFile(side, file), manualUploadChangeTab(tab), and manualUploadContinue(). Call manualUploadReset() to clear all selections and start the tab over.

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).

PropertyTypeDescription
phaseunion'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.
fileFile | nullThe PDF the user picked (null outside 'reviewing' / 'uploading' / 'holding').
fileNamestring | undefinedThe file's display name. Use this to render the preview row.
failReasonDigitalUploadFailReason | nullSet when phase === 'error'. Union: 'DIGITAL_ID_REQUESTED_BUT_OTHER_PROVIDED', 'ID_TYPE_UNACCEPTABLE', 'FILE_CHANGED_ERROR', 'INVALID_FILE_TYPE', 'NETWORK_ERROR', 'GENERIC'.
attemptsRemainingnumberRemaining upload retries (starts at 3). Hits 0 → phase === 'exhausted'.
uploadProgressnumberUpload progress 0–100. Only meaningful in 'uploading' / 'holding'.
pickerRequestIdnumberMonotonic 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. tutorialselecting (host opens picker on each pickerRequestId change) → reviewing (user confirms or replaces) → uploadingsuccess (auto-advances out of the sub-flow) or error → back to selecting, until attemptsRemaining === 0exhausted. 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).

Detection Status Values

StatusUser 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

MethodDescriptionWhen to Use
load()Starts the ID capture flowAlways call first
selectDocument(type)Selects 'id' or 'passport'When chooser
nextStep()Advances from tutorial to permissionsWhen tutorial
requestPermission()Requests camera accessWhen permissions.idle
goToLearnMore()Shows permission helpWhen permissions.idle
back()Goes back from learn moreWhen permissions.learnMore
capture()Manual capture triggerWhen detectionStatus === 'manualCapture'
switchToManualCapture()Switch from auto to manual modeDuring auto-capture
retryCapture()Retry after upload errorWhen canRetry is true
continueFromError()Continue after non-fatal errorWhen error allows continuation
continueToBack()Proceed to back captureWhen frontFinished
continueToFront()Flip back to front captureWhen backFinished
skipBack()Skip back captureWhen back is optional
acceptMandatoryConsent()Accept the regulation-required consentWhen mandatoryConsent
cancelMandatoryConsent()Decline the regulation-required consentWhen mandatoryConsent
manualUploadSelectFile(side, file)Pick a file for the manual upload fallbackWhen manualUpload, side: 'front' | 'back' | 'passport'
manualUploadChangeTab(tab)Switch between ID and Passport upload tabsWhen manualUpload and both tabs are shown
manualUploadContinue()Submit the selected manual-upload filesWhen manualUpload and state.canContinue is true
manualUploadReset()Clear all selected files on the active tab and start overWhen manualUpload
digitalUploadNextStep()Advance from the digital-upload tutorial to the pickerWhen digitalIdUpload and phase === 'tutorial'
digitalUploadPickFile(file)Hand a picked PDF to the SDK for reviewWhen digitalIdUpload and phase === 'selecting'
digitalUploadConfirm()Confirm the previewed file and start uploadingWhen digitalIdUpload and phase === 'reviewing'
digitalUploadReplace()Discard the previewed file and re-open the pickerWhen 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 fileTooLargeWhen digitalIdUpload and phase === 'fileTooLarge'
digitalUploadScanInstead()Abandon digital upload and fall back to live captureWhen digitalIdUpload (any phase, if allowed by config)
updateDetectionArea(area)Update detection boundsOn resize/orientation change
close()Close the flowAnytime
reset()Reset to initial stateAfter finished or error
stop()Cleanup resourcesWhen unmounting
getState()Returns current stateAnytime
subscribe(callback)Subscribe to state changesReturns unsubscribe function

React Example

import { useState, useEffect, useRef } from 'react';
import { createIdCaptureManager, type IdCaptureState } from '@incodetech/core/id';

function CustomIdCapture() {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [manager] = useState(() => createIdCaptureManager({
    config: { enableId: true, showTutorial: false, autoCaptureTimeout: 5 },
  }));
  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>
        </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 is IdCaptureConfig plus a required onCapture callback — captured at compile time, so a missing callback is a type error rather than a silent runtime no-op:

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,
  // ...the rest of IdCaptureConfig
  onCapture: async (response: CaptureOnlyResponse) => {
    // response.frontImage: IdCapturedImageData
    // response.backImage: IdCapturedImageData | undefined
    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:

type CaptureOnlyResponse = {
  frontImage: IdCapturedImageData;
  backImage: IdCapturedImageData | undefined;
};

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: when <incode-id> runs inside <incode-flow> (or createOrchestratedFlowManager), the orchestrator passes the full config from the dashboard automatically. The required marks below apply when you instantiate the module yourself via createIdCaptureManager({ config }) or set <incode-id>.config directly.

Document selection

OptionTypeRequiredDescription
enableIdbooleanEnable ID-card capture
enablePassportbooleanEnable passport capture
deviceWalletbooleanEnable device-wallet (digital ID) flow
digitalIdsUploadbooleanAllow uploading digital IDs as an alternative
manualUploadIdCapturebooleanEnable the manual file-upload fallback path
showDocumentChooserScreenbooleanRender the document-type chooser screen

Capture behavior

OptionTypeRequiredDescription
showTutorialbooleanShow tutorial before capture
onlyBackbooleanCapture the back side only
barcodeCapturebooleanUse barcode scanning when applicable
fetchAdditionalPagebooleanCapture an additional page after the main scan
autoCaptureTimeoutnumberSeconds before auto-capture triggers
deviceIdleTimeoutnumberSeconds of device idleness before timing out
captureAttemptsnumberMaximum retry attempts
enableIdRecordingbooleanEnable video recording of the capture
usSmartCapturebooleanUS-specific smart-capture mode
secondIdbooleanCapture a second ID document after the first
showCaptureButtonInAutobooleanShow a manual capture button during auto-capture
alwaysCaptureBackOfIdbooleanAlways capture the back of the ID, even when the front response would normally skip it

Per-country / per-document overrides

OptionTypeRequiredDescription
perCountryPerDocOverridesobjectNested map keyed by country code → document type → { onlyBack, fetchAdditionalPage }. Used to vary capture behavior by document. Pass {} if you don't need overrides. Whether to capture the back of the document is driven by the server's skipBackIdCapture upload response, not by an override here.

Extras (from IdCaptureConfig beyond TutorialIdConfig)

OptionTypeRequiredDescription
ageAssurancebooleanEnable age assurance features
mergeSessionRecordingsbooleanMerge per-step recordings into a single session-level recording
isDeepsightEnabledbooleanOverride Deepsight enablement (otherwise driven by the session)

IdCaptureConfig also exposes a few advanced fields (recording.capability, geometry, detectionArea, thresholds, settings, modelVersion, ds) for capability injection and ML-pipeline tuning. These are intended for Incode internal use and aren't documented here — consult the source if you need them.

Error Codes

CodeDescriptionUser Action
UPLOAD_ERRORUpload failedRetry capture
CLASSIFICATION_FAILEDDocument not recognizedUse clearer image
LOW_SHARPNESSImage too blurryHold device steadier
GLARE_DETECTEDGlare on documentAdjust lighting
WRONG_DOCUMENT_SIDEWrong side shownFlip document
ID_TYPE_UNACCEPTABLEDocument not supportedUse different ID
READABILITY_ISSUECannot read document fieldsImprove lighting or angle
RETRY_EXHAUSTED_CONTINUE_TO_BACKFront retries exhausted; proceeding to backAuto-advances
RETRY_EXHAUSTED_SKIP_BACKBack retries exhausted; skipping backAuto-advances
NO_MORE_TRIESMax attempts reachedFlow ends
UNEXPECTED_ERRORUnexpected internal errorRetry or contact support
NO_TOKENSession token missingRe-initialize SDK
PERMISSION_DENIEDCamera access deniedEnable in settings
USER_CANCELLEDUser cancelled the flowRe-open module
SERVER_ERRORServer-side errorRetry 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: IdCaptureConfig = {
  enableId: true,
  enablePassport: false,
  showTutorial: true,
  autoCaptureTimeout: 5,
};

Passport Only

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

See Also