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 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 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):
| 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:
ID_CAPTURE: runsidCaptureMachinefor the capture portion only, then advances without calling/process/id. Enabled by settingIdCaptureConfig.skipProcessId = true(the workflow engine sets this automatically on theID_CAPTUREnode's config).ID: runs the headlessid-verificationmodule (@incodetech/core/id-verification,createIdVerificationManager) which invokes/process/idon 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:
createIdVerificationManagerfrom@incodetech/core/id-verification: headless manager for the verification half (used by Workflow'sIDnode, 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-verificationimport 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 -->|nextStep| 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 | availableDocumentTypes |
loading |
Loading configuration | – |
tutorial |
Showing tutorial | selectedDocumentType, currentMode |
ageVerification |
Regulation-required age-confirmation gate before capture. Reached when config.ageAssurance === true. Advance with nextStep(). |
– |
permissions |
Camera permission handling | permissionStatus |
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 | – |
deviceWallet |
Wallet verification in progress or failed | phase, failReason, platform |
expired |
Document expired after processing | – |
manualUpload |
Manual file upload flow (see Manual Upload) | full shape below |
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 |
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.
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) |
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 |
Mandatory Consent State Properties
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).
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.
With onlyBack: true, the manual-upload screen exposes only the ID tab and back-of-ID slot. canContinue becomes true after the back upload succeeds. If manual upload is disabled, onlyBack skips the document chooser and starts camera capture on the back. If manual upload and showDocumentChooserScreen are enabled, the chooser shows only Identity Card and Upload ID.
| Property | Type | Description |
|---|---|---|
phase |
'selecting' | 'uploading' | 'exhausted' |
'selecting' while the user picks files, 'uploading' during upload, 'exhausted' after retries are spent. |
uploadingSide |
'front' | 'back' | 'passport' | undefined |
Which side is currently uploading; undefined outside phase === 'uploading'. |
activeTab |
'id' | 'passport' |
Currently selected tab. Use manualUploadChangeTab() to switch. |
showIdTab |
boolean |
Whether the ID tab should be rendered (driven by config). |
showPassportTab |
boolean |
Whether the Passport tab should be rendered. |
showFrontSlot |
boolean | undefined |
On the ID tab, whether to render the front-of-ID slot. Treat undefined as true for compatibility. It is false in back-only mode. |
showBackSlot |
boolean |
On the ID tab, whether to render the back-of-ID slot. It is false in front-only mode and true in back-only or two-sided mode. |
frontFileName |
string | undefined |
File name the user picked for the front of the ID, if any. |
backFileName |
string | undefined |
File name for the back of the ID, if any. |
passportFileName |
string | undefined |
File name for the passport upload, if any. |
frontUploaded |
boolean |
True once the front file has been accepted server-side. |
backUploaded |
boolean |
True once the back file has been accepted server-side. |
passportUploaded |
boolean |
True once the passport file has been accepted server-side. |
canContinue |
boolean |
Whether the Continue button should be enabled. Use this to gate manualUploadContinue(). |
retriesLeft |
number |
Remaining retries on the current tab. |
errorSide |
'front' | 'back' | 'passport' | null |
Slot associated with the current error; null if none. |
errorKey |
string | null |
i18n 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).
| 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 and wallet states
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. |
deviceWallet |
phase |
'requesting' | 'presenting' | 'decrypting' | 'failed'. Show progress for requesting/decrypting and wait for the device sheet during presenting. |
deviceWallet |
failReason |
'UNSUPPORTED_PLATFORM' | 'NO_CREDENTIAL' | 'WALLET_ERROR' | null. Render localized recovery guidance. |
deviceWallet |
platform |
'ios' | 'android' | undefined |
Wallet completion advances the ID manager to finished; identity attributes are retrieved through session results, not the wallet state. On failure, offer retry or another configured method. See the mDL headless example.
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
| 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 |
deviceWalletRetry() |
Retry wallet verification | deviceWallet, failed phase |
deviceWalletUseAnotherMethod() |
Return to the chooser, or close when no chooser is configured | deviceWallet, failed phase |
load() |
Starts the ID capture flow | Always call first |
selectDocument(type) |
Selects an entry from availableDocumentTypes |
When chooser |
nextStep() |
Advances from tutorial to permissions | When tutorial |
requestPermission() |
Requests camera access | When permissions.idle |
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 |
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 |
manualUploadSelectFile(side, file) |
Pick a file for the manual upload fallback | When manualUpload, side: 'front' | 'back' | 'passport' |
manualUploadChangeTab(tab) |
Switch between ID and Passport upload tabs | When manualUpload and both tabs are shown |
manualUploadContinue() |
Submit the selected manual-upload files | When manualUpload and state.canContinue is true |
manualUploadReset() |
Clear all selected files on the active tab and start over | When manualUpload |
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) |
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>
</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
// 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: <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 |
digitalIdsUpload |
boolean |
✅ | Allow uploading digital IDs as an alternative |
manualUploadIdCapture |
boolean |
✅ | Enable the manual file-upload fallback path. With onlyBack and the chooser enabled, the chooser contains only Identity Card and Upload ID. Manual upload accepts only the back. |
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. Without manual upload, the document chooser is skipped. With manual upload and the chooser enabled, it contains Identity Card and Upload ID. |
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
- Headless Mode: Complete headless API reference
- WASM Configuration: Setting up WebAssembly
- Individual Modules: Overview of all modules