# ID Capture Module

<WebSDK2 />

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](doc:web-sdk-2-module-patterns#2-camera-capture-modules), 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.

```ts
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:

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

See [WASM Configuration](doc:web-sdk-2-wasm) for self-hosted paths and the lower-level `warmupWasm()` API.

## Usage

### Vanilla HTML / TypeScript

```html
<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](doc:web-sdk-2-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](doc:web-sdk-2-framework-integration#react-19-shortcut).

```tsx
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](doc:web-sdk-2-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

```tsx
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

```mermaid
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](#mandatory-consent-state-properties)) |
| `processing`       | Server-side document processing                                                                                                   | –                                                                               |
| `expired`          | Document expired after processing                                                                                                 | –                                                                               |
| `manualUpload`     | Manual file upload flow (see [Manual Upload](#manual-upload-state-properties))                                                    | full shape below                                                                |
| `digitalIdUpload`  | Digital ID upload sub-flow — PDF e-IDs uploaded by file picker (see [Digital ID Upload](#digital-id-upload-state-properties))     | full shape below                                                                |
| `finished`         | Capture complete                                                                                                                  | –                                                                               |
| `error`            | Fatal error occurred                                                                                                              | `error`                                                                         |
| `closed`           | User closed the flow                                                                                                              | –                                                                               |

### Capture State Properties

When `status === 'capture'`:

| Property             | Type                | Description                                                                                 |
| -------------------- | ------------------- | ------------------------------------------------------------------------------------------- |
| `stream`             | `MediaStream`       | Camera stream for `<video>` element                                                         |
| `currentMode`        | `'front' \| 'back'` | Which side is being captured                                                                |
| `captureStatus`      | `string`            | `'initializing'`, `'detecting'`, `'capturing'`, `'uploading'`, `'uploadError'`, `'success'` |
| `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                                                              |
| `canRetry`           | `boolean`           | Whether retry is available                                                                  |
| `previewImageUrl`    | `string?`           | Captured image preview URL                                                                  |

### 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:

```typescript
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.

| 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.                                                                                                                                                                            |
| `showBackSlot`     | `boolean`                                      | On 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). |
| `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.                                                                                                                                                                                   |
| `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).

### 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                                                  |
| ------------------------------------ | ----------------------------------------------------------------- | ------------------------------------------------------------ |
| `load()`                             | Starts the ID capture flow                                        | Always call first                                            |
| `selectDocument(type)`               | Selects `'id'` or `'passport'`                                    | 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                                            | After `finished` or `error`                                  |
| `stop()`                             | Cleanup resources                                                 | When unmounting                                              |
| `getState()`                         | Returns current state                                             | Anytime                                                      |
| `subscribe(callback)`                | Subscribe to state changes                                        | Returns unsubscribe function                                 |

### React Example

```tsx
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:

```typescript
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:

```typescript
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

| Option                      | Type      | Required | Description                                   |
| --------------------------- | --------- | -------- | --------------------------------------------- |
| `enableId`                  | `boolean` | ✅        | Enable ID-card capture                        |
| `enablePassport`            | `boolean` | ✅        | Enable passport capture                       |
| `deviceWallet`              | `boolean` | ✅        | Enable device-wallet (digital ID) flow        |
| `digitalIdsUpload`          | `boolean` | ✅        | Allow uploading digital IDs as an alternative |
| `manualUploadIdCapture`     | `boolean` | ✅        | Enable the manual file-upload fallback path   |
| `showDocumentChooserScreen` | `boolean` | ✅        | Render the document-type chooser screen       |

### Capture behavior

| Option                    | Type      | Required | Description                                                                            |
| ------------------------- | --------- | -------- | -------------------------------------------------------------------------------------- |
| `showTutorial`            | `boolean` | ✅        | Show tutorial before capture                                                           |
| `onlyBack`                | `boolean` | ✅        | Capture the back side only                                                             |
| `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 video recording of the capture                                                  |
| `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 |

### Per-country / per-document overrides

| Option                      | Type   | Required | Description                                                                                                                                                                                                                                                                                                   |
| --------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `perCountryPerDocOverrides` | object | ✅        | Nested 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`)

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

`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

| 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](#usage).

### ID Card Only

```ts
const config: IdCaptureConfig = {
  enableId: true,
  enablePassport: false,
  showTutorial: true,
  autoCaptureTimeout: 5,
};
```

### Passport Only

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

***

## See Also

* [Headless Mode](doc:web-sdk-2-headless-mode): Complete headless API reference
* [WASM Configuration](doc:web-sdk-2-wasm): Setting up WebAssembly
* [Individual Modules](doc:web-sdk-2-individual-modules): Overview of all modules
