SDK reference · Incode Web SDK 2 Reference / Web SDK 2 Getting Started

Getting Started

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.

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
  • 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 below.

Install

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.

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

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

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

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.

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.


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.

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

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.

// 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 });
});
// 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
Reference for the <incode-flow> component IncodeFlow Component
Per-module headless API Headless Mode
Customize colors and typography Theming & Styling
Configure WASM hosting WASM Configuration
Reduce bundle size Bundle Optimization
Localize the SDK Internationalization
Subscribe to flow events Event Callbacks
Handle errors and edge cases Troubleshooting
Was this page helpful?