# Web SDK 2.0 Guide

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.

Headless mode lets you use the SDK's core logic without any pre-built UI. This is perfect for:

- Custom UI implementations with your own design system
- Backend-driven verification flows
- Automated testing
- React Native or other non-web platforms

## How It Works

Each verification module provides a **manager** that encapsulates state machine logic. You subscribe to state changes and call methods to drive the flow.

```
@incodetech/core

Your Application

subscribe()

load(), submit(), etc.

state updates

Custom UI

Manager

State Machine

Incode API
```

## Available Managers

| Module | Manager Factory | Import |
| --- | --- | --- |
| Phone | `createPhoneManager()` | `@incodetech/core/phone` |
| Email | `createEmailManager()` | `@incodetech/core/email` |
| Selfie | `createSelfieManager()` | `@incodetech/core/selfie` |
| ID Capture | `createIdCaptureManager()` | `@incodetech/core/id` |
| Orchestrated Flow | `createOrchestratedFlowManager()` | `@incodetech/core/flow` |

## Setup

Before using any manager, configure the SDK and activate your session token:

TypeScript

```typescript
import { setup } from '@incodetech/core';
import { initializeSession } from '@incodetech/core/session';

await setup({ apiURL: 'https://demo-api.incodesmile.com' });
await initializeSession({
  token: 'your-session-token' /* from createSession() */
});
```

`setup` provisions the HTTP client and (optionally) warms up WASM; `initializeSession` activates the session token and pre-loads session-scoped state. The two-call form lets you start `setup` (including WASM warmup) before the token is known. The one-shot form `setup({ apiURL, token })` is still supported as a convenience — it delegates to `initializeSession` for you.

## Phone Verification

### Quick Start

TypeScript

```typescript
import { createPhoneManager } from '@incodetech/core/phone';

// 1. Create manager with configuration
const manager = createPhoneManager({
  config: {
    otpVerification: true,
    otpExpirationInMinutes: 5,
    prefill: false,
  },
});

// 2. Subscribe to state changes
manager.subscribe((state) => {
  console.log('Status:', state.status);

if (state.status === 'finished') {
    console.log('Phone verified!');
    manager.stop();
  }
});

// 3. Start the flow
manager.load();

// 4. When state is 'inputting', set the phone number
manager.setPhoneNumber('+14155551234', true);
manager.submit();

// 5. When state is 'awaitingOtp', submit the OTP
manager.submitOtp('ABC123');
```

### State Machine Flow

```
load

submit

submitOtp

back

idle

inputting

awaitingOtp

finished
```

### States Reference

| Status | Description | Properties |
| --- | --- | --- |
| `idle` | Initial state | – |
| `loadingPrefill` | Fetching pre-filled phone | – |
| `inputting` | Ready for phone input | `countryCode`, `phonePrefix`, `phoneError?` |
| `submitting` | Submitting phone number | – |
| `sendingInitialOtp` | Sending first OTP | – |
| `resendingOtp` | Resending OTP | – |
| `awaitingOtp` | Waiting for OTP entry | `resendTimer`, `canResend`, `attemptsRemaining` |
| `verifyingOtp` | Verifying OTP code | `resendTimer`, `canResend` |
| `otpError` | OTP verification failed | `otpError`, `attemptsRemaining`, `resendTimer`, `canResend` |
| `finished` | Verification complete | – |
| `error` | Fatal error | `error` |

### API Methods

| Method | Description | When to Use |
| --- | --- | --- |
| `load()` | Initializes the flow | Always call first |
| `setPhoneNumber(phone, isValid)` | Sets phone number and validation state | When `inputting`, before `submit()` |
| `setOptInGranted(granted)` | Sets marketing opt-in preference | When `inputting`, if opt-in enabled |
| `submit()` | Submits the phone number | After setting valid phone |
| `setOtpCode(code)` | Sets OTP without submitting (controlled input) | When `awaitingOtp` |
| `submitOtp(code)` | Sets and submits OTP | When `awaitingOtp` or `otpError` |
| `resendOtp()` | Requests new OTP code | When `canResend` is true |
| `back()` | Returns to phone input | When `awaitingOtp` |
| `reset()` | Resets to initial state | After `finished` or `error` |
| `stop()` | Cleanup resources | When unmounting |
| `getState()` | Returns current state synchronously | Anytime |
| `subscribe(callback)` | Subscribe to state changes | Returns unsubscribe function |

### Configuration Options

TypeScript

```typescript
type PhoneConfig = {
  otpVerification: boolean; // Require OTP verification (default: true)
  otpExpirationInMinutes: number; // OTP validity in minutes (default: 5)
  prefill: boolean; // Fetch pre-filled phone from backend
  isInstantVerify?: boolean; // Use instant verification API
  optinEnabled?: boolean; // Show marketing opt-in checkbox
  maxOtpAttempts?: number; // Max OTP attempts (default: 3)
};
```

## Email Verification

Email verification works identically to phone verification.

### Quick Start

TypeScript

```typescript
import { createEmailManager } from '@incodetech/core/email';

const manager = createEmailManager({
  config: {
    otpVerification: true,
    otpExpirationInMinutes: 5,
    prefill: false,
  },
});

manager.subscribe((state) => {
  if (state.status === 'finished') {
    console.log('Email verified!');
    manager.stop();
  }
});

manager.load();
manager.setEmail('user@example.com', true);
manager.submit();

// When state is 'awaitingOtp'
manager.submitOtp('ABC123');
```

### State Machine Flow

```
load

submit

submitOtp

back

idle

inputting

awaitingOtp

finished
```

### States Reference

| Status | Description | Properties |
| --- | --- | --- |
| `idle` | Initial state | – |
| `loadingPrefill` | Fetching pre-filled email | – |
| `inputting` | Ready for email input | `prefilledEmail?`, `emailError?` |
| `submitting` | Submitting email | – |
| `sendingInitialOtp` | Sending first OTP | – |
| `resendingOtp` | Resending OTP | – |
| `awaitingOtp` | Waiting for OTP entry | `resendTimer`, `canResend`, `attemptsRemaining` |
| `verifyingOtp` | Verifying OTP code | `resendTimer`, `canResend` |
| `otpError` | OTP verification failed | `otpError`, `attemptsRemaining`, `resendTimer`, `canResend` |
| `finished` | Verification complete | – |
| `error` | Fatal error | `error` |

### API Methods

| Method | Description | When to Use |
| --- | --- | --- |
| `load()` | Initializes the flow | Always call first |
| `setEmail(email, isValid)` | Sets email and validation state | When `inputting`, before `submit()` |
| `submit()` | Submits the email | After setting valid email |
| `setOtpCode(code)` | Sets OTP without submitting | When `awaitingOtp` |
| `submitOtp(code)` | Sets and submits OTP | When `awaitingOtp` or `otpError` |
| `resendOtp()` | Requests new OTP code | When `canResend` is true |
| `back()` | Returns to email input | When `awaitingOtp` |
| `reset()` | Resets 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 |

## Selfie Capture

Selfie capture is more complex due to camera handling and ML-powered face detection. **Requires WASM configuration.**

### Quick Start

TypeScript

```typescript
import { setup } from '@incodetech/core';
import { initializeSession } from '@incodetech/core/session';
import { createSelfieManager } from '@incodetech/core/selfie';

// Preload the selfie WASM pipeline as part of setup() — Incode CDN
// defaults are used unless you override paths. See WASM Configuration
// for self-hosted paths and the lower-level warmupWasm() API.
await setup({
  apiURL: 'https://demo-api.incodesmile.com',
  wasm: { pipelines: ['selfie'] },
});
await initializeSession({ token: 'your-token' });

const manager = createSelfieManager({
  config: {
    showTutorial: true,
    autoCaptureTimeout: 10,
    validateLenses: true,
    validateFaceMask: true,
  },
});

manager.subscribe((state) => {
  switch (state.status) {
    case 'tutorial':
      // Show your tutorial UI
      // Call manager.nextStep() when user is ready
      break;

case 'permissions':
      // Show permission request UI
      if (state.permissionStatus === 'denied') {
        // Show instructions to enable camera
      }
      break;

case 'capture':
      // Camera is active
      // state.stream - MediaStream for <video> element
      // state.detectionStatus - Current face detection feedback
      // state.captureStatus - 'detecting', 'capturing', 'uploading', etc.
      break;

case 'finished':
      console.log('Selfie captured!', state.processResponse);
      manager.stop();
      break;

case 'error':
      console.error('Error:', state.error);
      break;
  }
});

manager.load();
```

### State Machine Flow

```
load

nextStep

granted

upload done

success

idle

tutorial

permissions

capture

processing

finished
```

### States Reference

| Status | Description | Properties |
| --- | --- | --- |
| `idle` | Initial state | – |
| `loading` | Checking permissions | – |
| `tutorial` | Showing tutorial | – |
| `permissions` | Handling camera access | `permissionStatus` |
| `capture` | Camera active | `stream`, `captureStatus`, `detectionStatus` |
| `processing` | Server-side processing of the captured selfie | – |
| `finished` | Capture complete | `processResponse?` |
| `closed` | User closed flow | – |
| `error` | Fatal error | `error` |

### Capture State Properties

When `status === 'capture'`, the following properties are available:

| Property | Type | Description |
| --- | --- | --- |
| `stream` | `CameraStream` | Camera stream for `<video>` element |
| `captureStatus` | `string` | Sub-state: `initializing`, `detecting`, `capturing`, `uploading`, `uploadError`, `success` |
| `detectionStatus` | `DetectionStatus` | Face detection feedback |
| `attemptsRemaining` | `number` | Remaining capture attempts |
| `uploadError` | `string?` | Error message if upload failed |
| `assistedOnboarding` | `boolean` | Whether assisted onboarding mode is active |
| `debugFrame` | `ImageData?` | Latest processed frame (for debugging) |

### API Methods

| Method | Description | When to Use |
| --- | --- | --- |
| `load()` | Starts the selfie flow | Always call first |
| `nextStep()` | Advances to next step | From `tutorial` to `permissions` |
| `requestPermission()` | Requests camera permission | When `permissions.idle` or `permissions.learnMore` |
| `goToLearnMore()` | Shows permission help | When `permissions.idle` |
| `back()` | Goes back from learn more | When `permissions.learnMore` |
| `capture()` | Manual capture | When `detectionStatus === 'manualCapture'` |
| `retryCapture()` | Retry after upload error | When `captureStatus === 'uploadError'` |
| `close()` | Closes the flow | Anytime |
| `reset()` | Resets 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 |

### Handling cancellation (`closed`)

When the user dismisses the selfie flow, the manager transitions to `closed`. **`closed` is a final state**: the manager won't re-emit further updates. Custom UIs need to decide what to do next.

```typescript
selfieManager.subscribe((state) => {
  if (state.status === 'closed') {
    // Optional: render a brief "Cancelled — continuing…" screen, then advance.
    setTimeout(() => flowManager.completeModule(), 800);
  }
});
```

### ID Capture

ID capture handles document scanning with ML-powered quality checks. **Requires WASM configuration.**

### Quick Start

TypeScript

```typescript
import { setup } from '@incodetech/core';
import { initializeSession } from '@incodetech/core/session';
import { createIdCaptureManager } from '@incodetech/core/id';

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

const manager = createIdCaptureManager({
  config: {
    showTutorial: true,
    enableId: true,
    enablePassport: false,
    autoCaptureTimeout: 5,
    captureAttempts: 3,
  },
});

manager.subscribe((state) => {
  switch (state.status) {
    case 'chooser':
      // Show document type selection UI
      break;

case 'tutorial':
      // Show tutorial for the selected document type
      break;

case 'permissions':
      // Handle camera permissions
      break;

case 'capture':
      // Camera is active
      break;

case 'finished':
      console.log('ID captured successfully!');
      manager.stop();
      break;

case 'error':
      console.error('Error:', state.error);
      break;
  }
});

manager.load();
```

### States Reference

| Status | Description | Properties |
| --- | --- | --- |
| `idle` | Initial state | – |
| `chooser` | Document type selection | `availableDocumentTypes` |
| `loading` | Loading configuration | – |
| `tutorial` | Showing tutorial | `selectedDocumentType` |
| `permissions` | Camera permission request | `permissionStatus` |
| `capture` | Active capture | `stream`, `currentMode`, `captureStatus`, `detectionStatus`, `counterValue` |
| `finished` | Capture complete | – |
| `closed` | User closed flow | – |
| `error` | Fatal error | `error` |

## Workflow Manager

The Workflow Manager runs **server-driven** multi-step onboarding workflows, where the next step (and its configuration) is chosen by the backend per session.

### Quick Start

TypeScript

```typescript
import { setup } from '@incodetech/core';
import { initializeSession } from '@incodetech/core/session';
import { createWorkflowManager } from '@incodetech/core/workflow';

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

const workflow = createWorkflowManager({
  interviewId: session.interviewId,
  isDesktop: matchMedia('(min-width: 768px)').matches,
  customModuleCallback: ({ name, interviewId, nodeId, onSuccess, onError }) => {
    // Run your custom business logic, then advance the workflow.
    onSuccess('handled');
  },
});

workflow.subscribe((state) => {
  switch (state.status) {
    case 'idle':
    case 'loading':
      if (state.homeScreen.visible) {
        // Render launch screen
      }
      break;
    case 'ready':
      // Render the module for state.currentNode.moduleKey
      break;
    case 'finished':
      workflow.stop();
      break;
  }
});

workflow.load();
```

### API Methods

| Method | Description |
| --- | --- |
| `load()` | Start loading the workflow configuration |
| `completeModule()` | Mark the current module as complete |
| `errorModule(error)` | Mark the current module as failed |
| `completeFlow()` | Skip the remaining nodes and go straight to the completion step |
| `continueFromHome()` | Advance past the launch / home screen |
| `getState()` | Get the current workflow state synchronously |
| `subscribe(callback)` | Subscribe to workflow state changes |

## Building Custom UI

### React Example: Phone Verification

```tsx
import { useState, useEffect } from 'react';
import { createPhoneManager, type PhoneState } from '@incodetech/core/phone';

function PhoneVerification() {
  const [manager] = useState(() =>
    createPhoneManager({
      config: {
        otpVerification: true,
        otpExpirationInMinutes: 5,
        prefill: false,
      },
    }),
  );
  const [state, setState] = useState<PhoneState>({ status: 'idle' });
  const [phone, setPhone] = useState('');
  const [otp, setOtp] = useState('');

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

const handlePhoneSubmit = () => {
    const isValid = phone.length >= 10; // Add real validation
    manager.setPhoneNumber(phone, isValid);
    manager.submit();
  };

switch (state.status) {
    case 'inputting':
      return (
        <div>
          <input
            type="tel"
            value={phone}
            onChange={(e) => setPhone(e.target.value)}
            placeholder="Enter phone number"
          />
          {state.phoneError && <p className="error">{state.phoneError}</p>}
          <button onClick={handlePhoneSubmit}>Send OTP</button>
        </div>
      );

case 'awaitingOtp':
      return (
        <div>
          <p>Enter the code sent to your phone</p>
          <input
            type="text"
            value={otp}
            onChange={(e) => setOtp(e.target.value)}
            placeholder="Enter OTP"
            maxLength={6}
          />
          <button onClick={() => manager.submitOtp(otp)}>Verify</button>
          {state.canResend ? (
            <button onClick={() => manager.resendOtp()}>Resend</button>
          ) : (
            <p>Resend in {state.resendTimer}s</p>
          )}
          <button onClick={() => manager.back()}>Change phone</button>
        </div>
      );

case 'otpError':
      return (
        <div>
          <p className="error">{state.otpError}</p>
          <p>Attempts remaining: {state.attemptsRemaining}</p>
          <input
            type="text"
            value={otp}
            onChange={(e) => setOtp(e.target.value)}
            placeholder="Enter OTP"
          />
          <button onClick={() => manager.submitOtp(otp)}>Try Again</button>
        </div>
      );

case 'finished':
      return <div>✅ Phone verified successfully!</div>;

case 'error':
      return <div className="error">Error: {state.error}</div>;

default:
      return <div>Loading...</div>;
  }
}
```

### React Example: Selfie Capture

```tsx
import { useState, useEffect, useRef } from 'react';
import { createSelfieManager, type SelfieState } from '@incodetech/core/selfie';

function SelfieCapture() {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [manager] = useState(() =>
    createSelfieManager({
      config: {
        showTutorial: true,
        autoCaptureTimeout: 10,
        validateLenses: true,
      },
    }),
  );
  const [state, setState] = useState<SelfieState>({ status: 'idle' });

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

// Connect stream to video element
  useEffect(() => {
    if (state.status === 'capture' && state.stream && videoRef.current) {
      videoRef.current.srcObject = state.stream;
    }
  }, [state]);

switch (state.status) {
    case 'tutorial':
      return (
        <div>
          <h2>Take a Selfie</h2>
          <ul>
            <li>Ensure good lighting</li>
            <li>Remove glasses and hats</li>
            <li>Look directly at the camera</li>
          </ul>
          <button onClick={() => manager.nextStep()}>Continue</button>
        </div>
      );

case 'permissions':
      if (state.permissionStatus === 'denied') {
        return (
          <div>
            <p>Camera access is required.</p>
            <p>Please enable camera in your browser settings.</p>
          </div>
        );
      }
      return (
        <div>
          <p>We need camera access to take your selfie.</p>
          <button onClick={() => manager.requestPermission()}>
            Allow Camera
          </button>
        </div>
      );

case 'capture':
      return (
        <div>
          <video ref={videoRef} autoPlay playsInline muted />
          <p>{getDetectionMessage(state.detectionStatus)}</p>
          {state.detectionStatus === 'manualCapture' && (
            <button onClick={() => manager.capture()}>Take Photo</button>
          )}
          {state.captureStatus === 'uploadError' && (
            <div>
              <p className="error">{state.uploadError}</p>
              <button onClick={() => manager.retryCapture()}>Retry</button>
            </div>
          )}
        </div>
      );

case 'finished':
      return <div>✅ Selfie captured successfully!</div>;

case 'error':
      return <div className="error">Error: {state.error}</div>;

default:
      return <div>Loading...</div>;
  }
}

function getDetectionMessage(status: string): string {
  const messages: Record<string, string> = {
    noFace: 'Position your face in the frame',
    tooFar: 'Move closer',
    tooClose: 'Move back',
    centerFace: 'Center your face',
    blur: 'Hold still, image is blurry',
    dark: 'Improve lighting conditions',
    lenses: 'Remove glasses or lenses',
    faceMask: 'Remove face mask',
    capturing: 'Capturing...',
    manualCapture: 'Ready - tap to capture',
  };
  return messages[status] || 'Detecting face...';
}
```

## TypeScript Support

All managers provide full TypeScript support with discriminated unions.

```typescript
import { type PhoneState } from '@incodetech/core/phone';

function handleState(state: PhoneState) {
  switch (state.status) {
    case 'inputting':
      console.log(`Country: ${state.countryCode}`);
      break;
    case 'awaitingOtp':
      console.log(`Resend in: ${state.resendTimer}s`);
      break;
    case 'error':
      console.error(state.error);
      break;
  }
}
```

## Manager Lifecycle

All managers follow the same lifecycle pattern:

```
Incode API
Manager
Your App
Incode API
Manager
Your App
loop
[User Interaction]
cleanup resources
createXxxManager(config)
subscribe(callback)
initial state
load()
fetch initial data
response
state update
setXxx() / submit()
API call
response
state update
stop()
```

## Dashboard Event Tracking

Track events to the Incode dashboard from your custom UI:

TypeScript

```typescript
import {
  addEvent,
  moduleOpened,
  moduleClosed,
  eventModuleNames,
} from '@incodetech/core/events';

// Track module lifecycle
useEffect(() => {
  moduleOpened(eventModuleNames.phone);
  return () => moduleClosed(eventModuleNames.phone);
}, []);

// Track custom events
addEvent({
  code: 'customButtonClicked',
  module: eventModuleNames.phone,
  payload: { buttonId: 'submit' },
});
```

## Best Practices

1. **Always clean up** – Call `manager.stop()` when unmounting components
2. **Handle all states** – Show appropriate UI for loading, error, and edge cases
3. **Validate inputs** – Pass `isValid` to `setPhoneNumber()` / `setEmail()` before `submit()`
4. **Use `getState()` sparingly** – Prefer subscribing to state changes
5. **Show feedback** – Use `detectionStatus` to guide users during capture
6. **Handle retries** – Check `attemptsRemaining` and `canRetry` for error recovery
