# Getting Started

<WebSDK2 />

This guide walks you through integrating the **Incode Web SDK** into your web application. It targets enterprise developers integrating Incode into their own product, and Incode Professional Services developers building custom integrations on a customer's behalf.

The SDK supports three integration paths — pick the one that matches how much UI control you need.

```mermaid
flowchart TD
    A[Need identity verification?] --> B{Want any control over the UI?}
    B -->|No, just drop it in| P1["<b>Path 1</b><br/>incode-flow web component<br/>~5 lines of code"]
    B -->|Yes| C{Want to use Incode's UI for the modules themselves?}
    C -->|Yes: own the shell, use Incode's screens| P2["<b>Path 2</b><br/>Orchestrator + module web components<br/>~30 lines of code"]
    C -->|No: fully custom UI| P3["<b>Path 3</b><br/>Headless Managers<br/>~50+ lines of code"]

    style P1 fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e
    style P2 fill:#fef3c7,stroke:#d97706,color:#78350f
    style P3 fill:#fce7f3,stroke:#be185d,color:#831843
```

***

## Prerequisites

Before you begin, ensure you have:

* **Incode Account**: access to the [Incode Dashboard](https://dashboard.incodesmile.com)
* **API Key**: found in the dashboard under Settings → API Keys
* **Configuration ID**: the flow ID from your dashboard configuration
* **Node.js 18+** and a modern bundler (Vite, Webpack, etc.) for module-aware imports

> **Production note:** Never expose your API key in client-side code. Create sessions from your backend and pass only the resulting token to the browser. See [Backend session creation](#production-backend-session-creation) below.

## Install

```bash
npm install @incodetech/web @incodetech/core
# or
pnpm add @incodetech/web @incodetech/core
# or
yarn add @incodetech/web @incodetech/core
```

`@incodetech/core` is the framework-agnostic SDK core (state machines, managers, business logic). `@incodetech/web` is the UI layer — every consumer-facing element ships as a standard Web Component plus its CSS.

## Initialize the SDK

Every integration path starts the same way: call `setup()`, create a session, then activate the session.

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

// 1. Configure the SDK (apiURL, optional WASM/i18n/UI options)
await setup({ apiURL: 'https://demo-api.incodesmile.com/0' }); // Use /0 if called from UI.

// 2. Create a session — in production this should happen on your backend (see below)
const session = await createSession('YOUR_API_KEY', {
  configurationId: 'YOUR_CONFIGURATION_ID',
  language: 'en-US',
});

// 3. Activate the session token
await initializeSession({ token: session.token });
```

> **Why three calls?** `setup()` provisions the HTTP client and (optionally) warms up WASM; `createSession` opens the verification interview and returns a token; `initializeSession` activates the token and pre-loads session-scoped state (feature flags, device fingerprint). If your backend hands you the token directly, you can collapse steps 2 and 3 into one `initializeSession({ token })` call. `setup({ apiURL, token })` is also still supported as a one-shot convenience — it delegates to `initializeSession` for you — but the two-call form makes the boot / activation boundary explicit, which matters when you want to start `setup()` early (e.g. to warm up WASM) before the token is known.
>
> **End-to-end encryption.** Pass `encryption: true` to `setup()` to opt into encrypted transport for every SDK request. It's not a self-serve flag — your Incode account team provisions the environment and gives you a dedicated `apiURL`, an `mgf1` scheme, and an API-key transmission choice. See [End-to-End Encryption](doc:web-sdk-2-e2ee) for the full walkthrough.

***

## Which path is right for me?

|                    | Path 1:  `<incode-flow>`       | Path 2:  Orchestrator + components      | Path 3:  Headless                                       |
| ------------------ | ------------------------------ | --------------------------------------- | ------------------------------------------------------- |
| **UI you own**     | None                           | Shell only                              | Everything                                              |
| **UI Incode owns** | Everything                     | Module screens                          | None                                                    |
| **Lines of code**  | ~5                             | ~30                                     | ~50+                                                    |
| **Flow logic**     | Internal                       | Orchestrator                            | Manual or orchestrator                                  |
| **Best for**       | Fastest TTV, standard branding | Custom shell, Incode's tested module UX | Bespoke UI, native shells, accessibility customizations |
| **Branding**       | Theme tokens only              | Full shell, theme tokens for modules    | Full                                                    |

***

## Path 1: `<incode-flow>` drop-in

The simplest integration. Drop one custom element on the page, set its config, get a callback when verification finishes. Incode owns the entire UI: home screen, all module screens, transitions, completion.

### HTML / vanilla JS

```html
<!doctype html>
<html>
  <head>
    <style>
      html, body, #root, incode-flow { height: 100%; margin: 0; }
    </style>
  </head>
  <body>
    <div id="root"></div>

    <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';            // registers <incode-flow>
      import '@incodetech/web/flow/styles.css';

      await setup({ apiURL: 'https://demo-api.incodesmile.com/0' }); // Use /0 if called from UI.

      const session = await createSession('YOUR_API_KEY', {
        configurationId: 'YOUR_CONFIGURATION_ID',
        language: 'en-US',
      });

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

      const flow = document.createElement('incode-flow');
      flow.config = {
        token: session.token,
        lang: 'en-US',
        enableHome: true,
      };
      flow.onFinish = (result) => {
        console.log('Verification complete:', result);
        // result.action: 'approved' | 'rejected' | 'none'
        // result.scoreStatus: 'OK' | 'WARN' | 'MANUAL_OK' | 'FAIL' | ...
        // result.redirectionUrl: string
      };
      flow.onError = (error, errorCode) => {
        console.error('Flow error:', error, errorCode);
      };

      document.getElementById('root').appendChild(flow);
    </script>
  </body>
</html>
```

### React

`<incode-flow>` is a standard custom element, so you can use it from React directly. Set non-string properties (`config`, `onFinish`, `onError`) imperatively via a ref — JSX attributes only support strings.

> **React 18 or earlier:** add the JSX augmentation from [Framework Integration → TypeScript: JSX support for `incode-*` tags](doc:web-sdk-2-framework-integration#typescript-jsx-support-for-incode--tags) so TypeScript recognizes the Incode tags. React 19+ handles unknown lowercase tags automatically and can also use the simpler form from [Framework Integration → React 19+ shortcut](doc:web-sdk-2-framework-integration#react-19-shortcut).

```typescript
import { useEffect, useRef } from 'react';
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';

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

export function VerificationFlow({ 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) => console.log('Done:', result);
    el.onError = (error, code) => console.error(error, code);
  }, [token]);

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

### What `<incode-flow>` config accepts

| Field                                | Type                           | Required | Notes                                                                   |
| ------------------------------------ | ------------------------------ | -------- | ----------------------------------------------------------------------- |
| `token`                              | `string`                       | ✅        | Session token from `createSession()`                                    |
| `lang`                               | `string`                       |          | Locale, 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`                   |          | Custom WASM paths (see [WASM Configuration](doc:web-sdk-2-wasm))        |
| `spinnerConfig`                      | `{ title?, subtitle?, size? }` |          | Customize the loading spinner copy                                      |
| `onFlowEvent`                        | `(event) => void`              |          | Stream of flow milestones (`flow.ready`, `flow.module.completed`, etc.) |
| `onModuleLoading` / `onModuleLoaded` | `(moduleKey) => void`          |          | Hooks around lazy-loaded module chunks                                  |

Self-loading variant: pass `apiKey` + `configurationId` instead of `token` and the component will create the session itself. Convenient for prototyping; **avoid in production** because it puts the API key in the browser.

***

## Path 2: Orchestrator + module web components

You own the shell — landing page, transitions, completion screen — but Incode owns each module's UI. Use the orchestrator to drive flow progression and render Incode's module web components in your own layout.

```typescript
import { useEffect, useState } from 'react';
import { setup } from '@incodetech/core';
import { createSession } from '@incodetech/core/session';
import {
  createOrchestratedFlowManager,
  type FlowModuleConfig,
  type OrchestratedFlowState,
} from '@incodetech/core/flow';
import { selfieMachine } from '@incodetech/core/selfie';
import { phoneMachine } from '@incodetech/core/phone';
import { emailMachine } from '@incodetech/core/email';
import { idCaptureMachine } from '@incodetech/core/id';
import '@incodetech/web/themes/light.css';
import '@incodetech/web/base.css';
import '@incodetech/web/selfie';
import '@incodetech/web/phone';
import '@incodetech/web/email';
import '@incodetech/web/id';

const flowManager = createOrchestratedFlowManager({
  modules: {
    SELFIE: selfieMachine,
    PHONE: phoneMachine,
    EMAIL: emailMachine,
    ID: idCaptureMachine,
  },
});

export function OrchestratedFlow({ token }: { token: string }) {
  const [state, setState] = useState<OrchestratedFlowState>(
    flowManager.getState(),
  );

  useEffect(() => {
    const unsubscribe = flowManager.subscribe(setState);
    flowManager.load();
    return () => {
      unsubscribe();
      flowManager.stop();
    };
  }, []);

  if (state.status === 'idle' || state.status === 'loading') {
    return <YourLoadingScreen />;
  }

  if (state.status === 'error') {
    return <YourErrorScreen message={state.error} onRetry={() => flowManager.reset()} />;
  }

  if (state.status === 'finished') {
    return <YourCompletionScreen result={state.finishStatus} />;
  }

  // status === 'ready' — render the current module
  const { currentStep, config } = state;

  if (currentStep === 'PHONE') {
    return (
      <incode-phone
        ref={(el) => {
          if (!el) return;
          el.config = config as FlowModuleConfig['PHONE'];
          el.onFinish = () => flowManager.completeModule();
          el.onError = (err) => console.error(err);
        }}
      />
    );
  }

  if (currentStep === 'SELFIE') {
    return (
      <incode-selfie
        ref={(el) => {
          if (!el) return;
          el.config = config as FlowModuleConfig['SELFIE'];
          el.onFinish = () => flowManager.completeModule();
          el.onError = (err) => console.error(err);
        }}
      />
    );
  }

  // ... handle EMAIL, ID, etc.
  return null;
}
```

### What you control

* **The shell:** your landing page, your branding, your loading and error states, your completion view
* **When the flow starts:** call `flowManager.load()` only after the user clicks "Start"
* **When to advance:** call `flowManager.completeModule()` on each module's `onFinish`
* **Reset / retry:** `flowManager.reset()` returns to idle; the user can begin the flow again

### Orchestrator state shape

Every variant carries `homeScreen` (visibility of the SDK's built-in home screen, when enabled) and `presentation` (UI hints used by `<incode-flow>` for transition timing). For Path 2 you can usually ignore these and let your shell own the loading/transition UX.

```ts
type OrchestratedFlowHomeScreen = {
  visible: boolean;
  isContinueLoading: boolean;
};

type OrchestratedFlowPresentation = {
  isAwaitingReady: boolean;
  lazyModuleKey: string | undefined;
  shouldPrefetchHome: boolean;
};

type OrchestratedFlowState =
  | { status: 'idle';
      homeScreen: OrchestratedFlowHomeScreen;
      presentation: OrchestratedFlowPresentation }
  | { status: 'loading';
      homeScreen: OrchestratedFlowHomeScreen;
      presentation: OrchestratedFlowPresentation }
  | { status: 'ready';
      flow: Flow;
      steps: string[];
      currentStepIndex: number;
      currentStep: string | undefined;
      config: unknown;
      moduleState: unknown;
      homeScreen: OrchestratedFlowHomeScreen;
      presentation: OrchestratedFlowPresentation }
  | { status: 'finished';
      flow: Flow;
      finishStatus: FinishStatus;
      homeScreen: OrchestratedFlowHomeScreen;
      presentation: OrchestratedFlowPresentation }
  | { status: 'error';
      error: string;
      homeScreen: OrchestratedFlowHomeScreen;
      presentation: OrchestratedFlowPresentation };
```

`config` and `moduleState` are typed as `unknown` because they vary by module. Cast via `FlowModuleConfig[<step>]` (e.g. `FlowModuleConfig['PHONE']`) when handing them to the corresponding component.

> **WASM warmup:** Path 2 lets you load WASM only for modules that actually need it. Use `getRequiredWasmPipelines(state.flow)` (from `@incodetech/core/flow`) plus `warmupWasm` (from `@incodetech/core/wasm`) on the `'ready'` transition. See [WASM Configuration](doc:web-sdk-2-wasm).

***

## Path 3: Headless Managers

You own everything — including each module's UI. Use the per-module Manager APIs to drive state machines directly, subscribe to state, and render whatever interface you want (React Native via web view, AR overlay, server-rendered HTML, terminal UI, anything).

This is the most code, but also the most flexibility.

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

await setup({ apiURL: 'https://demo-api.incodesmile.com/0' }); // Use /0 if called from UI.

const session = await createSession('YOUR_API_KEY', {
  configurationId: 'YOUR_CONFIGURATION_ID',
});

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

const phoneManager = createPhoneManager({
  config: {
    otpVerification: true,
    otpExpirationInMinutes: 5,
    prefill: false,
    maxOtpAttempts: 3,
  },
});

const unsubscribe = phoneManager.subscribe((state) => {
  switch (state.status) {
    case 'inputting':
      renderPhoneInputForm({
        countryCode: state.countryCode,
        onSubmit: (phone, isValid) => {
          phoneManager.setPhoneNumber(phone, isValid);
          phoneManager.submit();
        },
      });
      break;
    case 'awaitingOtp':
      renderOtpForm({
        canResend: state.canResend,
        timer: state.resendTimer,
        onSubmit: (otp) => phoneManager.submitOtp(otp),
        onResend: () => phoneManager.resendOtp(),
      });
      break;
    case 'finished':
      console.log('Phone verified!');
      unsubscribe();
      phoneManager.stop();
      break;
    case 'error':
      console.error('Phone error:', state.error);
      break;
  }
});

phoneManager.load();
```

### Manager API surface (every module)

All managers follow the same shape:

| Method                | Purpose                                                      |
| --------------------- | ------------------------------------------------------------ |
| `getState()`          | Snapshot of current state                                    |
| `subscribe(callback)` | Listen to state transitions; returns an unsubscribe function |
| `load()`              | Start the state machine                                      |
| `stop()`              | Tear down — always call before unmount                       |

Plus module-specific methods (e.g. `setPhoneNumber`, `submit`, `submitOtp`, `requestPermission`, `capture`, `retryCapture`, etc.). See [Headless Mode](doc:web-sdk-2-headless-mode) for per-module API references and state shapes.

### Composing modules headlessly

You can either drive each module manager directly (as above) or use `createOrchestratedFlowManager` _without_ rendering any of Incode's UI components — subscribe to the orchestrator's state and render your own UI for every module. Same pattern as Path 2; you just don't import `@incodetech/web/*` at all.

***

## Result handling

When verification finishes, you get a `FinishStatus`:

```ts
type FinishStatus = {
  redirectionUrl: string;
  action: 'approved' | 'rejected' | 'none';
  scoreStatus:
    | 'OK'
    | 'WARN'
    | 'MANUAL_OK'
    | 'MANUAL_FAIL'
    | 'FAIL'
    | 'UNKNOWN';
};
```

| `scoreStatus` | Meaning                              |
| ------------- | ------------------------------------ |
| `OK`          | Passed all checks                    |
| `WARN`        | Passed with minor concerns           |
| `MANUAL_OK`   | Needs manual review, leaning approve |
| `MANUAL_FAIL` | Needs manual review, leaning reject  |
| `FAIL`        | Failed verification                  |
| `UNKNOWN`     | Status not yet determined            |

`onFinish` on the client is a notification, not the source of truth. Always confirm the verification outcome from your backend by calling Incode's server API with the session token — client-side data can be tampered with.

***

## Production: backend session creation

`@incodetech/core` is a frontend SDK and shouldn't be imported on the server. Instead, call Incode's REST API directly from your backend with whichever HTTP client your stack uses. The endpoint your backend hits is the same one the frontend `createSession()` helper wraps:

```
POST {apiURL}/omni/start
Headers:
  x-api-key: <your API key>
  api-version: 1.0
  content-type: application/json
Body:
  { "configurationId": "<flow ID>", "externalId": "<your user reference>", "language": "en-US" }
```

The response is JSON with at least `{ token, interviewId }`. Forward only the `token` to the browser.

```ts
// Backend (Node.js 18+ — uses native fetch; works the same with axios or any HTTP client)
app.post('/api/incode/start', async (req, res) => {
  const response = await fetch('https://demo-api.incodesmile.com/omni/start', {
    method: 'POST',
    headers: {
      'x-api-key': process.env.INCODE_API_KEY,
      'api-version': '1.0',
      'content-type': 'application/json',
    },
    body: JSON.stringify({
      configurationId: process.env.INCODE_CONFIG_ID,
      externalId: req.user.id, // link the session to your user
    }),
  });

  if (!response.ok) {
    return res.status(response.status).json({ error: 'Failed to create Incode session' });
  }

  const { token } = await response.json();
  res.json({ token });
});
```

```ts
// Frontend
const response = await fetch('/api/incode/start', { method: 'POST' });
const { token } = await response.json();

await setup({ apiURL: 'https://demo-api.incodesmile.com/0' }); // Use /0 if called from UI.
await initializeSession({ token });
// proceed with Path 1, 2, or 3
```

***

## Next steps

| Goal                                        | Where to look                                              |
| ------------------------------------------- | ---------------------------------------------------------- |
| See every module the SDK ships              | [Individual Modules](doc:web-sdk-2-individual-modules)     |
| Reference for the `<incode-flow>` component | [IncodeFlow Component](doc:web-sdk-2-incodeflow-component) |
| Per-module headless API                     | [Headless Mode](doc:web-sdk-2-headless-mode)               |
| Customize colors and typography             | [Theming & Styling](doc:web-sdk-2-theming)                 |
| Configure WASM hosting                      | [WASM Configuration](doc:web-sdk-2-wasm)                   |
| Reduce bundle size                          | [Bundle Optimization](doc:web-sdk-2-bundle-optimization)   |
| Localize the SDK                            | [Internationalization](doc:web-sdk-2-i18n)                 |
| Subscribe to flow events                    | [Event Callbacks](doc:web-sdk-2-event-callbacks)           |
| Handle errors and edge cases                | [Troubleshooting](doc:web-sdk-2-troubleshooting)           |
