📘

This guide is specific to Web SDK 2.0. If you are still using 1.x, you can find documentation [here](https://developer.incode.com/docs/web-sdk-reference). Contact your Incode Representative for upgrade information and check if you are a candidate for this upgrade.

Full rollout to all clients still TBD.

`<incode-flow>` is the complete, orchestrated identity verification experience as a standard Web Component. It runs the flow sequence configured in your Incode dashboard end-to-end.

**Important:** create the session via `createSession()` (or your backend) and call `setup()` with the resulting token before mounting `<incode-flow>`. The component does not create sessions internally; it consumes the session token you provide.

## Tag

```ts
// Side-effect import — registers the <incode-flow> custom element
import '@incodetech/web/flow';
import '@incodetech/web/flow/styles.css';
```

## Properties

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

| Property     | Type                                          | Required | Description                                                  |
|--------------|-----------------------------------------------|----------|--------------------------------------------------------------|
| `config`     | `FlowConfig`                                 | ✅       | Flow configuration object (see below)                       |
| `onFinish`   | `(result?: FinishStatus) => void`           | ✅       | Called when the flow completes                               |
| `onError`    | `(error: string | undefined, errorCode?: number) => void` | ❌      | Called on fatal errors                                      |

### FlowConfig

`FlowConfig` has two shapes — a token-based variant (recommended) and a self-loading variant. The two are mutually exclusive.

**Token variant (recommended)** — pass a token obtained from `createSession()` (ideally created on your backend):

| Property         | Type          | Required | Description                                                                             |
|------------------|---------------|----------|-----------------------------------------------------------------------------------------|
| `token`          | `string`      | ✅       | Session token                                                                          |
| `lang`           | `string`      | ❌       | Language code (e.g., 'en-US', 'es-MX')                                               |
| `enableHome`     | `boolean`     | ❌       | Show the SDK's built-in home screen                                                  |
| `authHint`       | `string`      | ❌       | QR/auth hint when re-entering a flow                                                  |
| `wasmConfig`     | `WasmConfig`  | ❌       | WASM paths (see [WASM Configuration](https://developer.incode.com/docs/web-sdk-2-wasm)) |
| `spinnerConfig`  | `SpinnerConfig` | ❌       | Loading spinner customization                                                           |
| `disableDashboardTheme` | `boolean` | ❌      | Skip applying the dashboard's theme tokens                                             |
| `urlUuid`       | `string`      | ❌       | QR anti-phishing token from the URL (mobile leg)                                      |
| `useCPF`        | `boolean`     | ❌       | Switch the orchestrator's `ID_OCR` step to `CPF_OCR` for Brazilian CPF flows. When `true`, `<incode-flow>` renders the CPF OCR module instead of the standard ID OCR module at that step. Has no effect when the dashboard flow has no `ID_OCR` step. |
| `onFlowEvent`   | `(event: FlowEvent) => void` | ❌ | Curated flow milestones                                                        |
| `onModuleLoading` | `(moduleKey: string) => void` | ❌ | Module begins loading (lazy chunk)                                              |
| `onModuleLoaded` | `(moduleKey: string) => void` | ❌ | Module loaded                                                                    |
| `onWasmWarmup`  | `(pipelines: string[]) => void` | ❌ | WASM warmup begins                                                                |
| `onUrlUuidRefreshed` | `(urlUuid: string) => void` | ❌ | New `urlUuid` available — host should update the address bar                      |

**Self-loading variant** — convenient for prototyping; **avoid in production** because it puts the API key in the browser:

| Property         | Type          | Required | Description                                                                             |
|------------------|---------------|----------|-----------------------------------------------------------------------------------------|
| `apiKey` (or `clientId`) | `string`  | ✅       | Auth key for session creation                                                        |
| `configurationId` | `string`   | ✅       | Dashboard flow ID                                                                    |
| `externalId`, `externalCustomerId`, `customFields`, `uuid`, `urlUuid`, `interviewId` | various | ❌ | Forwarded to `createSession()`                                                    |
| Plus any of the optional fields from the token variant above (`lang`, `enableHome`, callbacks, etc.) |  |  |                                             |

> **`apiURL` is NOT in `FlowConfig`.** It belongs on the global `setup()` call. The component reuses the API URL configured there.

## Basic Usage

### Step 1: Create the session

Create the session via `createSession()` (typically from your backend) and configure the SDK:

```tsx
import { setup } from '@incodetech/core';
import { createSession, initializeSession } from '@incodetech/core/session';

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

const session = await createSession(apiKey, {
  configurationId: 'your-config-id',
  language: 'en-US',
  externalId: 'optional-external-id',
});

await initializeSession({ token: session.token });
```

The `token` is still passed to the component itself via `flowElement.config.token` (see Step 2) — `initializeSession` activates it on the SDK's HTTP client; the component reads it from its own config. `setup({ apiURL, token })` is also supported as a one-shot convenience and delegates to `initializeSession` internally.

### Step 2: Mount the component

**HTML / vanilla TypeScript:**

```html
<incode-flow id="flow"></incode-flow>

<script type="module">
  import '@incodetech/web/themes/light.css';
  import '@incodetech/web/base.css';
  import '@incodetech/web/flow';
  import '@incodetech/web/flow/styles.css';

const flow = document.getElementById('flow');
  flow.config = { token: session.token, lang: 'en-US', enableHome: true };
  flow.onFinish = (result) => {
    if (result?.redirectionUrl) {
      window.location.href = result.redirectionUrl;
    }
  };
  flow.onError = (error, code) => console.error('Flow error:', error, code);
</script>
```

**React:**

```tsx
import { useEffect, useRef } from 'react';
import type { FlowConfig } from '@incodetech/web/flow';
import type { FinishStatus } from '@incodetech/core/flow';
import '@incodetech/web/flow';
import '@incodetech/web/flow/styles.css';

type FlowElement = HTMLElement & {
  config: FlowConfig;
  onFinish: (result?: FinishStatus) => void;
  onError: (error: string | undefined, code?: number) => void;
};

export function FlowMount({ token }: { token: string }) {
  const ref = useRef<FlowElement>(null);

useEffect(() => {
    const el = ref.current;
    if (!el) return;
    el.config = { token, lang: 'en-US', enableHome: true };
    el.onFinish = (result) => {
      if (result?.redirectionUrl) window.location.href = result.redirectionUrl;
    };
    el.onError = (error, code) => console.error('Flow error:', error, code);
  }, [token]);

return (
    <incode-flow ref={ref} style={{ display: 'block', height: '100vh' }} />
  );
}
```

### Complete React Example

```tsx
import { useState, useEffect, useRef } from 'react';
import { setup } from '@incodetech/core';
import { createSession, initializeSession } from '@incodetech/core/session';
import type { FlowConfig } from '@incodetech/web/flow';
import type { FinishStatus } from '@incodetech/core/flow';
import '@incodetech/web/themes/light.css';
import '@incodetech/web/base.css';
import '@incodetech/web/flow';
import '@incodetech/web/flow/styles.css';

function MyVerificationFlow() {
  const [sessionToken, setSessionToken] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const flowRef = useRef<FlowElement>(null);

useEffect(() => {
    const initialize = async () => {
      try {
        await setup({ apiURL: 'https://demo-api.incodesmile.com' });
        const session = await createSession('your-api-key', {
          configurationId: 'your-config-id',
          language: 'en-US',
        });
        await initializeSession({ token: session.token });
        setSessionToken(session.token);
      } catch (err) {
        setError(
          err instanceof Error ? err.message : 'Failed to create session',
        );
      }
    };
    initialize();
  }, []);

useEffect(() => {
    const el = flowRef.current;
    if (!el || !sessionToken) return;
    el.config = { token: sessionToken, lang: 'en-US', enableHome: true };
    el.onFinish = (result) => console.log('Flow completed:', result);
    el.onError = (error) => console.error('Flow error:', error);
  }, [sessionToken]);

if (error) return <div>Error: {error}</div>;
  if (!sessionToken) return <div>Initializing...</div>;

return (
    <incode-flow ref={flowRef} style={{ display: 'block', height: '100vh' }} />
  );
}
```

## Custom Start and Completion Screens

The `IncodeFlow` component does not render start or completion screens internally — you control what the user sees before the flow begins and after it finishes.

### Start screen

Build your own. Render whatever start UI suits your product (logo, copy, terms, a "Begin verification" button), and only mount `<incode-flow>` once the user clicks through. There is no public SDK component for this, by design — the start screen is a branding/UX surface that belongs to the integrator.

### Completion screen

For completion, the SDK ships an optional drop-in web component, `<incode-flow-completed>`, that renders a simple "verification complete" screen with optional auto-redirect. It is registered as a side effect of `@incodetech/web/flow` (the same import you already have for `<incode-flow>`):

```html
<incode-flow-completed
  title="All Done!"
  message="Your identity has been verified."
  redirection-url="https://yourapp.com/return"
  action="approved"
  score-status="OK"
  redirect-delay="3000"
></incode-flow-completed>
```

**Attributes:**

| Attribute            | Type      | Description                                                |
|----------------------|-----------|------------------------------------------------------------|
| `title`              | `string`  | Heading text. Falls back to a translated default.         |
| `message`            | `string`  | Subtitle text. Falls back to a translated default.        |
| `redirection-url`    | `string`  | If set, the page redirects here after `redirect-delay` ms.|
| `redirect-delay`      | `number`  | Milliseconds to wait before redirecting. Default `3000`.  |
| `disable-redirect`    | `boolean` | Skip the redirect even if `redirection-url` is set.       |
| `redirect-origin-only` | `boolean` | Suppress redirect when the page is already a redirect target (`?isRedirect=true`).|
| `action`             | `'approved' | 'rejected' | 'none'` | The verification outcome (used for analytics/styling hooks).|
| `score-status`       | `'OK' | 'WARN' | 'MANUAL_OK' | 'FAIL' | 'UNKNOWN' | 'MANUAL_FAIL'` | The score classification.     |
| `flow-id`           | `string`  | Appended to the redirect URL as a query param.             |
| `interview-id`      | `string`  | Appended to the redirect URL as a query param when redirecting cross-origin. |

If you'd rather build your own completion screen, the result data you need is on the `onFinish` payload from `<incode-flow>` (`redirectionUrl`, `action`, `scoreStatus`, `flowId`, `interviewId`). Render whatever you want and skip `<incode-flow-completed>` entirely.

### Language

Set the UI language via the `lang` property in your config:

```ts
flow.config = { token: session.token, lang: 'es' /* Spanish */ };
```

The SDK ships translations for ~85 locales (including regional variants like `en-DG`, `es-MX`, `pt-BR`, `zh-TW`, `fr-CA`). See [Internationalization](https://developer.incode.com/docs/web-sdk-2-i18n) for the full list, fallback behavior, and dashboard interaction.

## Event Callbacks

### onFinish

**Required callback** called when the flow finishes successfully. The component does not render a completion screen — you must handle the finish status yourself:

```ts
flow.onFinish = (result) => {
  console.log('Action:', result?.action); // 'approved' | 'rejected' | 'none'
  console.log('Status:', result?.scoreStatus); // 'OK' | 'WARN' | 'FAIL' | ...
  console.log('Redirect:', result?.redirectionUrl);

if (result?.action === 'approved' && result.redirectionUrl) {
    window.location.href = result.redirectionUrl;
  } else if (result?.action === 'rejected') {
    // Show rejection message
  }
};
```

The `result` object (typed as `FinishStatus`) contains:

- `redirectionUrl`: URL to redirect to (if configured)
- `action`: `'approved' | 'rejected' | 'none'`
- `scoreStatus`: `'OK' | 'WARN' | 'MANUAL_OK' | 'FAIL' | 'UNKNOWN' | 'MANUAL_FAIL'`

Note that `result` may be `undefined` (the callback signature is `(result?: FinishStatus) => void`), so always null-check before reading fields.

### onError

Called when an error occurs:

```ts
flow.onError = (error, code) => {
  console.error('Verification error:', error, code);
  // Show your own error UI or retry logic
};
```

### onModuleLoading / onModuleLoaded

Track lazy-loading of each module's UI chunk via config callbacks:

```ts
flow.config = {
  token: session.token,
  onModuleLoading: (moduleKey) => console.log(`Loading module: ${moduleKey}`),
  onModuleLoaded: (moduleKey) => console.log(`Loaded module: ${moduleKey}`),
};
```

### onWasmWarmup

Called when ML models begin loading:

```ts
flow.config = {
  token: session.token,
  onWasmWarmup: (pipelines) => {
    console.log('Warming up:', pipelines); // e.g., ['selfie', 'idCapture']
  },
};
```

## WASM Configuration

For ML-powered modules (selfie, ID capture), preload WASM with the `wasm` option on `setup()` so models are ready by the time the user reaches a camera step. The Incode CDN serves sensible defaults — you only need to pick which pipelines to preload:

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

// Then mount <incode-flow> as shown in Basic Usage above.
```

If you omit the `wasm` option, the SDK loads WASM lazily when the user reaches the first selfie or ID-capture step. Pass `wasm: false` to explicitly disable preload (useful when this page only runs phone/email flows). For self-hosted paths, custom model files, or the lower-level `warmupWasm()` API, see [WASM Configuration](https://developer.incode.com/docs/web-sdk-2-wasm).

## Loading Spinner Customization

`<incode-flow>` shows a transition spinner during initialization, while a module's lazy chunk loads, and between flow steps. The same spinner is used in every phase.

**Default text behavior:** if you don't pass `spinnerConfig`, the spinner renders with **no title or subtitle** — only the spinning icon. As soon as you set either `spinnerConfig.title` or `spinnerConfig.subtitle`, the missing field falls back to the SDK's translated defaults (i18n keys `loadingCircle.holdOn` for the title, `loadingCircle.validating` for the subtitle, which display as "Hold on" / "Validating" in English).

### SpinnerConfig Options

You can override the default text using `spinnerConfig`:

| Property    | Type       | Description                                                  |
|-------------|------------|--------------------------------------------------------------|
| `title`     | `string`   | Custom title text (overrides default)                        |
| `subtitle`  | `string`   | Custom subtitle text (overrides default)                     |
| `size`      | `'small' | 'medium' | 'large'` | Size of the spinner icon (default: 'medium')               |

### Custom Text

```ts
flow.config = {
  token: session.token,
  spinnerConfig: {
    title: 'Verifying your identity',
    subtitle: "This won't take long...",
    size: 'large',
  },
};
```

### CSS Variable Customization

You can also customize the spinner appearance using CSS variables:

```css
:root {
  /* Spinner colors */
  --spinner-surface-primary: #0066cc;
  --spinner-surface-secondary: #e6f0ff;

/* Spinner text */
  --spinner-text-title: #1a1a1a;
  --spinner-text-subtitle: #666666;

/* Spinner overlay background */
  --spinner-surface-overlay: #ffffff;
  --spinner-surface-overlay-opacity: 1;
}
```

See [Theming & Styling](https://developer.incode.com/docs/web-sdk-2-theming) for more details on CSS customization.

## Web Component

`IncodeFlow` is also available as a standard Web Component for framework-agnostic usage:

```html
<!DOCTYPE html>
<html>
  <head>
    <style>
      html,
      body,
      incode-flow {
        height: 100%;
        margin: 0;
      }
      incode-flow {
        display: block;
      }
    </style>
  </head>
  <body>
    <incode-flow></incode-flow>

<script type="module">
      import { setup } from '@incodetech/core';
      import {
        createSession,
        initializeSession,
      } from '@incodetech/core/session';
      import '@incodetech/web/themes/light.css';
      import '@incodetech/web/base.css';
      import '@incodetech/web/flow';
      import '@incodetech/web/flow/styles.css';

async function initializeFlow() {
        try {
          await setup({ apiURL: 'https://demo-api.incodesmile.com' });

const session = await createSession('your-api-key', {
            configurationId: 'your-config-id',
            language: 'en-US',
          });

await initializeSession({ token: session.token });

const flow = document.querySelector('incode-flow');
          flow.config = {
            token: session.token,
            lang: 'en-US',
            enableHome: true,
          };
          flow.onFinish = (result) => {
            if (result.action === 'approved' && result.redirectionUrl) {
              window.location.href = result.redirectionUrl;
            }
          };
          flow.onError = (error, code) => console.error('Error:', error, code);
        } catch (error) {
          console.error('Failed to initialize:', error);
        }
      }

initializeFlow();
    </script>
  </body>
</html>
```

## TypeScript

Full type definitions are included. Import `FlowConfig` from `@incodetech/web/flow` and `FinishStatus` from `@incodetech/core/flow`:

```typescript
import type { FlowConfig } from '@incodetech/web/flow';
import type { FinishStatus } from '@incodetech/core/flow';
import '@incodetech/web/flow';

// Automatically typed via HTMLElementTagNameMap augmentation
const flow = document.createElement('incode-flow');
flow.config = {
  // ✅ typed as FlowConfig
  token: 'session-token-from-createSession',
  lang: 'en-US',
};
flow.onFinish = (result) => {
  // ✅ result typed as FinishStatus | undefined
  console.log(result?.action);
};
```

See [TypeScript Types](https://developer.incode.com/docs/web-sdk-2-typescript-types#web-component-element-types) for more details.

## Unsupported Modules

If the orchestrated flow returns a step whose UI module isn't bundled in this SDK build, `<incode-flow>` renders a fallback screen titled **"Module not available"** with a **Next** button. Clicking Next calls `flowManager.completeModule()` and advances the flow.

This keeps the rest of a multi-step flow working when one step is unrecognized — useful when the dashboard configuration runs ahead of the SDK release. It applies only to the `<incode-flow>` component; in headless mode (`createOrchestratedFlowManager` with your own UI), an unrecognized step key throws "No registered module found for: <KEY>".
