npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@seontechnologies/seon-orchestration

v0.0.3

Published

An advanced SDK for natural person identification through document scanning, facial recognition, designed for secure and efficient user verification.

Readme

SEON Orchestration SDK for Web

1. Integration Guide

The SEON Orchestration SDK enables you to integrate identity verification workflows into your web application. The integration follows a secure three-party flow: your frontend initiates verification, your backend securely communicates with SEON's API to generate session tokens, and the SDK handles the user-facing verification process.

Orchestration Flow


1.1 Getting Started

Installation

npm install @seontechnologies/seon-orchestration
# or
yarn add @seontechnologies/seon-orchestration

Prerequisites

  • Node.js >=20.0.0, npm >=7.0.0
  • SEON account with workflow access
  • API key (obtain from Admin Panel → Settings → API Keys)
  • At least one workflow created (Admin Panel → Workflows)

Browser Compatibility

| Browser | Min Version | | ------------------- | :---------: | | Chrome | 96 | | Safari | 15 | | Firefox | 79 | | Opera | 82 | | iOS Safari | 15 | | Android Browser | 81 | | Chrome for Android | 96 | | Firefox for Android | 79 |

⚠️ Internet Explorer is not supported.


1.2 Configuration

FlowConfiguration

interface FlowConfiguration {
  token: string; // Required
  language?: 'en' | 'de' | 'es' | 'fr' | 'it' | 'pt' | 'hu' | 'ae' | 'cn'; // Default: 'en'
  theme?: ThemeConfiguration; // Custom theming
  renderingMode?: 'fullscreen' | 'inline' | 'popup'; // Default: 'fullscreen'
  containerId?: string; // Required when renderingMode is 'inline'
}

For getting the token, see Backend Workflow Init


1.2.1 Theming

Theme Configuration Structure

interface ThemeConfiguration {
  light?: ColorScheme; // Colors for light mode
  dark?: ColorScheme; // Colors for dark mode
  fontFamily?: string; // Custom font family name
  fontUrl?: string; // URL to load custom font (WOFF2 recommended)
  fontWeight?: string; // Font weight (e.g., '400', '500', '600')
}

interface ColorScheme {
  baseTextOnLight?: string; // Text color on light backgrounds
  baseTextOnDark?: string; // Text color on dark backgrounds
  baseAccent?: string; // Primary accent/brand color
  baseOnAccent?: string; // Text color on accent backgrounds
  logoUrl?: string; // URL to custom logo image
}

Color Guidelines

| Property | Purpose | Contrast Requirement | | ----------------- | ------------------------------------- | ------------------------------ | | baseTextOnLight | Body text, labels on light surfaces | Min 4.5:1 ratio | | baseTextOnDark | Text on dark surfaces (e.g., buttons) | Min 4.5:1 ratio | | baseAccent | Primary buttons, links, focus states | — | | baseOnAccent | Text/icons on accent-colored elements | Min 4.5:1 against baseAccent |

⚠️ Accessibility: Ensure color contrast meets WCAG 2.1 AA standards (4.5:1 for normal text).

Dark Mode

  • Provide both light and dark objects for full dark mode support

Typography

// Example: Using a custom Google Font
{
  theme: {
    fontFamily: 'Inter',
    fontUrl: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap',
    fontWeight: '500'
  }
}

| Constraint | Limit | | ------------ | ------------------------------------------- | | Font source | Google Fonts URLs only | | Font weights | 400–700 recommended | | Font loading | Loaded async; system font shown until ready |

Logo & Branding

You can provide a custom logo for each color scheme:

{
  theme: {
    light: {
      logoUrl: 'https://example.com/logo-dark.svg'  // Logo for light mode (typically darker logo)
    },
    dark: {
      logoUrl: 'https://example.com/logo-light.svg' // Logo for dark mode (typically lighter logo)
    }
  }
}

| Constraint | Recommendation | | ------------- | ----------------------------------------------- | | Format | SVG preferred; PNG/JPEG supported | | Dimensions | Max height ~40px recommended | | Hosting | Must be publicly accessible HTTPS URL | | Color schemes | Provide separate logos for light and dark modes |

Complete Theming Example

await SeonOrchestration.start({
  token,
  language: 'en',
  theme: {
    light: {
      baseTextOnLight: '#1a1a1a',
      baseTextOnDark: '#ffffff',
      baseAccent: '#0066cc',
      baseOnAccent: '#ffffff',
      logoUrl: 'https://example.com/logo-dark.svg',
    },
    dark: {
      baseTextOnLight: '#e5e5e5',
      baseTextOnDark: '#1a1a1a',
      baseAccent: '#4d9fff',
      baseOnAccent: '#000000',
      logoUrl: 'https://example.com/logo-light.svg',
    },
    fontFamily: 'Inter',
    fontUrl: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap',
    fontWeight: '500',
  },
});

1.2.2 Localization

Setting the Language

await SeonOrchestration.start({
  token,
  language: 'de', // Language code
});

Supported Languages

| Code | Language | | ---- | ------------ | | en | English | | de | German | | es | Spanish | | fr | French | | it | Italian | | pt | Portuguese | | hu | Hungarian | | ae | Arabic (UAE) | | cn | Chinese |

⚠️ Default: If language is not specified or an unsupported code is provided, the SDK defaults to English (en).

📝 Note: Custom translation overrides are available for enterprise customers via SEON Support.

Language Detection

// Option 1: Use browser's preferred language
const browserLang = navigator.language.split('-')[0]; // e.g., 'en-US' -> 'en'

await SeonOrchestration.start({
  token,
  language: browserLang,
});

// Option 2: Use your app's language setting
await SeonOrchestration.start({
  token,
  language: userPreferences.language || 'en',
});

Custom Strings

📍 Custom string overrides are available through SEON Support.


1.2.3 Rendering Modes & Embedding

Available Rendering Modes

| Mode | Description | Use Case | | ------------ | ---------------------------------- | --------------------------------------------------- | | fullscreen | Takes over entire viewport | Default. Best for mobile web, single-purpose flows | | popup | Opens in a new browser window | Desktop apps where you want to keep main UI visible | | inline | Renders inside a container element | Embedded within your existing page layout |

Fullscreen Mode (Default)

await SeonOrchestration.start({
  token,
  renderingMode: 'fullscreen', // or omit (default)
});
  • Covers the entire viewport
  • Best camera/document capture experience
  • Recommended by default

Popup Mode

await SeonOrchestration.start({
  token,
  renderingMode: 'popup',
});
  • Opens in 800×600 px window (or available screen size)
  • User must allow popups in browser
  • Handle "Failed to open popup window..." error gracefully

Inline Mode

// HTML: <div id="verification-container"></div>

await SeonOrchestration.start({
  token,
  renderingMode: 'inline',
  containerId: 'verification-container', // Required for inline
});

| Requirement | Details | | ----------------- | ------------------------------------------------------------- | | Container element | Must exist in DOM before start() is called | | Minimum size | 400×600 px recommended for usability | | Responsive | Container should be responsive; SDK adapts to available space |


1.3 API Reference

Core Methods

| Method | Description | | --------------------------------------- | --------------------------------------------------- | | SeonOrchestration.start(config) | Start verification flow | | SeonOrchestration.close() | Close the current verification flow and clean up UI | | SeonOrchestration.on(event, handler) | Subscribe to events | | SeonOrchestration.off(event, handler) | Unsubscribe from events |

Events

| Event | Callback Signature | Description | | ----------- | ----------------------------------- | ---------------------- | | opened | () => void | Flow UI opened | | closed | () => void | Flow UI closed | | started | () => void | Verification started | | completed | (status: CompletionTypes) => void | Verification completed | | cancelled | () => void | User cancelled | | error | (errorCode: ErrorCodes) => void | Error occurred |

Completion Types: 'success' | 'pending' | 'failed' | 'unknown'

Error Codes (via error event)

| Code | Trigger Condition | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | error_code_1 | Device not supported — No capable camera/device found, or general error screen dismissed | | error_code_2 | (Reserved — not currently used) | | error_code_3 | Authentication failed — Unauthorized request to backend (invalid/expired token, see Backend Workflow Init) | | error_code_4 | Document capture SDK error — Failed to initialize or load document scanning SDK | | error_code_5 | Document capture retry limit exceeded — User exceeded max retries for document scanning | | error_code_6 | Liveness check retry limit exceeded — User exceeded max retries for liveness detection | | unknown | Unhandled error — Unexpected JavaScript error or unhandled promise rejection |

SDK Exceptions (via try/catch on start())

| Error Message | Trigger Condition | | ------------------------------------------------------------------- | ----------------------------------------------------------------- | | "IDV flow is already running." | Calling start() when a flow is already active | | "Configuration is not set." | Calling start() without passing config | | "Failed to initialize client: {status} {statusText}" | Backend init failed (e.g., invalid/expired token returns 401/403) | | "Invalid response from client init." | Invalid account configuration, please contact SEON Support | | "Container ID is required for inline rendering." | Using renderingMode: 'inline' without containerId | | "Container element with id '{id}' not found." | Container DOM element doesn't exist | | "Failed to open popup window. Please allow popups and try again." | Browser blocked popup window | | "Invalid rendering mode specified." | Invalid renderingMode value |


1.4 Integration Examples

Minimal Example

import { SeonOrchestration } from '@seontechnologies/seon-orchestration';

// 1. Get token from YOUR backend (keeps API keys secure)
const { token } = await fetch('/api/init-verification', { method: 'POST' }).then((r) => r.json());

// 2. Start verification
await SeonOrchestration.start({ token, language: 'en' });

Backend example

Full React Example

import React, { useEffect, useState } from 'react';
import { SeonOrchestration, CompletionTypes, ErrorCodes } from '@seontechnologies/seon-orchestration';

// Note: onComplete and onError should be wrapped in useCallback by the parent
// component to prevent unnecessary effect re-runs
export function VerificationComponent({ userId, onComplete, onError }) {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const handleCompleted = (status: CompletionTypes) => {
      onComplete(status);
    };
    const handleError = (errorCode: ErrorCodes) => {
      setError(`Error: ${errorCode}`);
      onError(errorCode);
    };
    const handleClosed = () => setIsLoading(false);

    SeonOrchestration.on('completed', handleCompleted);
    SeonOrchestration.on('error', handleError);
    SeonOrchestration.on('closed', handleClosed);

    return () => {
      SeonOrchestration.off('completed', handleCompleted);
      SeonOrchestration.off('error', handleError);
      SeonOrchestration.off('closed', handleClosed);
    };
  }, [onComplete, onError]);

  const startVerification = async () => {
    setIsLoading(true);
    setError(null);
    try {
      const response = await fetch('/api/init-verification', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId }),
      });
      const { token } = await response.json();
      await SeonOrchestration.start({ token, language: 'en' });
    } catch (err) {
      setError(err.message);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div>
      {error && <div style={{ color: 'red' }}>{error}</div>}
      <button onClick={startVerification} disabled={isLoading}>
        {isLoading ? 'Starting...' : 'Start Verification'}
      </button>
    </div>
  );
}

Vanilla JS Example

import { SeonOrchestration } from '@seontechnologies/seon-orchestration';

// Set up event listeners
SeonOrchestration.on('completed', (status) => {
  if (status === 'success') alert('Verification successful!');
  else if (status === 'pending') alert('Verification pending review');
  else alert('Verification failed');
});

SeonOrchestration.on('error', (errorCode) => {
  console.error('Verification error:', errorCode);
});

// Start verification function
async function startVerification() {
  const button = document.getElementById('start-btn');
  button.disabled = true;
  button.textContent = 'Starting...';

  try {
    const response = await fetch('/api/init-verification', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ userId: 'user-123' }),
    });
    const { token } = await response.json();
    await SeonOrchestration.start({ token, language: 'en' });
  } catch (error) {
    alert('Failed to start verification');
  } finally {
    button.disabled = false;
    button.textContent = 'Start Verification';
  }
}

document.getElementById('start-btn').addEventListener('click', startVerification);

Backend Workflow Init (for reference)

POST {baseUrl}/v1/init-workflow
Headers: { 'x-api-key': 'SEON-API-KEY' }
Body: { workflowId: 'YOUR_WORKFLOW_ID', inputs: { user_id: '...', ... } }
Response: { data: { token: '...' } }

Required fields: workflowId (UUID) and inputs.user_id are always mandatory. See Workflow Inputs Schema for additional required fields based on your workflow configuration.

⚠️ Security: Never expose API keys in frontend. Always init workflows via your backend.


1.5 Error Handling & Troubleshooting

Common Errors

| Error | Cause | Solution | | ------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------ | | "IDV flow is already running." | Calling start() twice | Wait for flow to complete or close first | | "Configuration is not set." | Missing config object | Pass { token } to start() | | "Failed to open popup window..." | Browser blocked popup | User must allow popups; consider fullscreen mode | | "Invalid rendering mode specified." | Typo in mode | Use 'fullscreen', 'inline', or 'popup' | | "Container element not found" | DOM element missing | Ensure container exists before start() | | "Failed to initialize client: 401" | Invalid/expired token | Generate a fresh token from your backend (see Backend Workflow Init) | | "MISSING_REQUIRED_INPUTS" | Workflow inputs missing | Check required fields for your workflow configuration |

Debugging Failed Sessions

Step 1: Identify the Session

The /v1/init-workflow response includes an executionId which uniquely identifies the workflow execution. Store this alongside your user_id for debugging:

// Store the executionId from init-workflow response
const { data } = await response.json();
const { executionId, token } = data;

// Log or store for later debugging
console.log(`Started verification for user ${userId}: execution ${executionId}`);

Step 2: Check SDK Events

Log all events for debugging:

const events = ['opened', 'closed', 'started', 'completed', 'cancelled', 'error'];

events.forEach((event) => {
  SeonOrchestration.on(event, (...args) => {
    console.log(`[SEON] ${event}:`, ...args);
  });
});

Step 3: Review in Admin Panel

  1. Navigate to Transactions → Workflow Runs
  2. Search by user_id or executionId
  3. Click on the workflow run to view:
    • Step-by-step execution timeline
    • Individual check results
    • Error details and failure reasons
    • Captured media (documents, selfies)

Correlation IDs

Use these fields to correlate workflow runs across your systems:

| Field | Purpose | Where to Find | | ------------- | -------------------- | ---------------------------------------------------------------------------------- | | user_id | Your user identifier | Provided in init-workflow inputs | | executionId | Workflow run ID | Returned from init-workflow response (shown as "Workflow run ID" in Admin Panel) |

Best Practice: Store the executionId returned from init-workflow for debugging and support queries:

// Backend: init-workflow response handling
const response = await initWorkflow({
  workflowId: 'your-workflow-id',
  inputs: {
    user_id: 'user-123',
    // ... other inputs
  },
});

// Store executionId for correlation
const { executionId } = response;
console.log(`Workflow started: ${executionId}`);

API Error Responses

| HTTP Status | Error Code | Meaning | | :---------: | ------------------------- | -------------------------------------------- | | 400 | MISSING_REQUIRED_INPUTS | Required workflow inputs not provided | | 400 | INVALID_INPUT_FORMAT | Input field format is incorrect | | 401 | UNAUTHORIZED | Invalid or missing API key | | 403 | FORBIDDEN | API key doesn't have access to this workflow | | 404 | WORKFLOW_NOT_FOUND | Workflow ID doesn't exist or is inactive | | 429 | RATE_LIMITED | Too many requests; implement backoff | | 500 | INTERNAL_ERROR | Contact SEON support with correlation IDs |

📝 Note: Rate limits depend on your plan. See SEON API Rate Limits for default limits, or contact SEON for details on your specific plan limits.


1.6 Workflow Inputs Schema (Backend Reference)

Endpoint

POST {baseUrl}/v1/init-workflow

Base URLs

| Environment | URL | | ----------- | ----------------------------------------------------------- | | EU | https://api.seon.io/orchestration-api | | US | https://api.us-east-1-main.seon.io/orchestration-api | | APAC | https://api.ap-southeast-1-main.seon.io/orchestration-api |

Headers

x-api-key: YOUR_SEON_API_KEY
Content-Type: application/json

Request Body

{
  workflowId: string; // UUID - Required
  inputs: {
    // Required
    user_id: string; // Always required — your user's unique identifier
    // ... additional fields based on workflow configuration (see below)
  }
}

Response

{
  data: {
    executionId: string; // Unique workflow execution ID (UUID)
    token: string; // Pass this to the frontend SDK
  }
}

1.6.1 Admin Panel Node Names

| Admin Panel Label | | ------------------------- | | Data enrichment | | Document verification | | Selfie verification | | Address verification | | Condition | | End workflow |

Input Source Dropdown Options

When configuring nodes, users select where input data comes from:

| Dropdown Option | Meaning | | ------------------------------ | ----------------------------------------------- | | "Sent with session trigger" | Value must be provided in init-workflow request | | "Collected from previous step" | Value is taken from a previous workflow step |


1.6.3 Input Requirements by Node Type

The following shows which inputs become required based on the nodes present in your workflow. The system validates these at init-workflow time and returns MISSING_REQUIRED_INPUTS error if validation fails.

Data Enrichment (FRAUD_CHECK)

| Field | Required When | Auto-Captured | Admin Panel Toggle | | -------------- | --------------------- | :-----------: | --------------------------------------------------------------------------------------------------------- | | email | Email check is ON | ❌ | "Email check" | | phone_number | Phone check is ON | ❌ | "Phone check" | | ip | Never required | ✅ | "IP check" — tooltip: "By default, workflow input is used. If missing, the end user's IP is collected." |

Note: Device fingerprinting is handled automatically by the SDK when "Device check" is enabled. No manual input needed.

AML Check

| Field | Required | Admin Panel Setting | | --------------------- | :---------: | ----------------------------------------------------------------------------------- | | user_fullname | Conditional | "AML check" → Full name dropdown (required when set to "Sent with session trigger") | | user_dob | - | Optional enrichment field | | user_pob | - | Optional enrichment field | | user_photoid_number | - | Optional enrichment field | | user_country | - | Optional enrichment field |

Legend: Required = Always required | Conditional = Required in specific scenarios | - = Optional

eKYC Check

eKYC Country Matrix:

| Country | ID Type | Code | Required Fields | Optional Fields | Validation | | ---------- | ----------------- | --------- | ---------------------------------------------------- | ------------------------------------------------ | ----------------------- | | 🇳🇬 Nigeria | National ID | NIN | user_firstname, user_lastname, user_dob, nin | user_middlename, user_gender, phone_number | nin: 11 digits | | 🇳🇬 Nigeria | Bank Verification | BVN | user_firstname, user_lastname, user_dob, bvn | user_middlename, user_gender, phone_number | bvn: 11 digits | | 🇧🇷 Brazil | Tax ID | CPF | cpf | user_fullname*, user_dob | cpf: 123.456.789-00 | | 🇲🇽 Mexico | Population ID | CURP | user_dob, curp, user_fullname* | — | curp: 18 characters | | 🇺🇸 USA | Social Security | SSN | user_dob, ssn, user_fullname* | — | ssn: 123-45-6789 | | 🇮🇳 India | Aadhaar | AADHAAR | aadhaar | — | aadhaar: 12 digits |

* user_fullname is required only when set to "Sent with session trigger"

eKYC Response Data:

eKYC checks return a boolean result indicating whether the provided identity data was successfully verified.

Selfie Verification (SELFIE_CHECK)

| Field | Required | Required When | Admin Panel Setting | | ----------------- | :---------: | ------------------------------------------------------------------------------------ | ---------------------------------------------------- | | reference_image | Conditional | "Face match" is ON and Reference image is set to "Sent with session trigger" | Checkbox: "Face match" → Dropdown: "Reference image" |

When Reference image is set to "Collected from previous step", the system uses the document photo from a previous Document verification node.

Document Verification (DOCUMENT_CHECK)

| Field | Required | Required When | | --------------- | :---------: | --------------------------------------------------- | | user_fullname | Conditional | Full name is set to "Sent with session trigger" |

Condition (BRANCHING)

| Field | Required | Behavior When Missing | | -------------- | :------: | ------------------------------------ | | {propertyId} | - | Workflow follows the else branch |

Note: Condition properties are never strictly required. If a property used for branching is not provided, the workflow automatically takes the else branch.


1.6.4 Quick Reference: Common Workflow Scenarios

| Workflow Type | Required Inputs | | ------------------------------------- | --------------------------------------------------------------- | | Document + Selfie (basic) | user_id only | | Document + Selfie + Face Match (URL) | user_id, reference_image | | Email + Phone fraud check | user_id, email, phone_number | | Full fraud check (Email + Phone + IP) | user_id, email, phone_number (IP auto-captured) | | AML screening | user_id, user_fullname | | NIN eKYC (Nigeria) | user_id, user_firstname, user_lastname, user_dob, nin | | BVN eKYC (Nigeria) | user_id, user_firstname, user_lastname, user_dob, bvn | | CPF eKYC (Brazil) | user_id, cpf |


1.6.5 All Available Input Fields

Legend: Required = Always required | Conditional = Required in specific scenarios | - = Optional

User Information

| Field | Type | Required | Description | | --------------------- | -------- | :---------: | --------------------------------------------------------------------------------------- | | user_id | string | Required | User's unique identifier in your system | | email | string | Conditional | Full email address. Required if Email check enabled | | phone_number | string | Conditional | Phone with country code (max 19 chars). Required if Phone check enabled | | user_fullname | string | Conditional | Full name (can be hashed). Required for AML/POA if set to "Sent with session trigger" | | user_firstname | string | Conditional | First name. Required for NIN/BVN eKYC | | user_middlename | string | - | Middle name | | user_lastname | string | Conditional | Last name. Required for NIN/BVN eKYC | | user_dob | string | Conditional | Date of birth (YYYY-MM-DD). Required for NIN/BVN/CURP/SSN eKYC | | user_pob | string | - | Place of birth | | user_photoid_number | string | - | Photo ID number | | user_country | string | - | ISO 3166-1 two-char country code | | user_city | string | - | City name | | user_region | string | - | ISO 3166-2 two-char region code | | user_zip | string | - | Postal/zip code | | user_street | string | - | Street address line 1 | | user_street2 | string | - | Street address line 2 | | user_address | string | Conditional | Full address. Required for POA if set to "Sent with session trigger" | | user_gender | string | - | User gender | | user_created | number | - | Registration date (UNIX timestamp) |

Session & Device

| Field | Type | Required | Description | | ----------- | -------- | :------: | --------------------------------------------------------------- | | ip | string | - | User's IP address. Auto-captured from browser if not provided | | session | string | - | Device fingerprint. Auto-collected by SDK | | device_id | string | - | Third-party device fingerprint ID |

Identity Verification

| Field | Type | Required | Description | | ----------------- | -------- | :---------: | ----------------------------------------------------------------------------------------------------------- | | reference_image | string | Conditional | Reference image URL or previous session ID. Required for face match if set to "Sent with session trigger" | | nin | string | Conditional | Nigerian National ID Number (11 digits). Required for NIN eKYC | | bvn | string | Conditional | Bank Verification Number (11 digits). Required for BVN eKYC | | cpf | string | Conditional | Brazilian tax identifier. Required for CPF eKYC | | curp | string | Conditional | Mexican tax identifier (18 chars). Required for CURP eKYC | | ssn | string | Conditional | US Social Security Number. Required for SSN eKYC | | aadhaar | string | Conditional | Indian Aadhaar identifier (12 digits). Required for Aadhaar eKYC |

Transaction Information

| Field | Type | Required | Description | | ---------------------- | -------- | :------: | ------------------------------------ | | transaction_id | string | - | Unique transaction identifier | | transaction_type | string | - | Transaction type (e.g., 'purchase') | | transaction_amount | number | - | Amount (decimal, e.g., 539.99) | | transaction_currency | string | - | ISO 4217 currency code (e.g., 'USD') |

Payment Information

| Field | Type | Required | Description | | ------------------ | -------- | :------: | -------------------------------------- | | payment_id | string | - | Payment transaction ID (max 100 chars) | | payment_mode | string | - | Payment method (e.g., 'paypal') | | payment_provider | string | - | Payment provider (e.g., 'skrill') | | card_bin | string | - | First 4-9 digits of card | | card_last | string | - | Last 4 digits of card | | card_fullname | string | - | Name on card |

Shipping & Billing Address

(All optional)

  • Shipping: shipping_country, shipping_city, shipping_region, shipping_zip, shipping_street, shipping_street2, shipping_phone, shipping_fullname, shipping_method
  • Billing: billing_country, billing_city, billing_region, billing_zip, billing_street, billing_street2, billing_phone

Custom Data

| Field | Type | Required | Description | | --------------- | -------- | :------: | ----------------------------- | | custom_fields | object | - | Key-value pairs for ML system | | items | array | - | List of purchased items |


1.6.6 Example Requests

Minimal (Document + Selfie):

{
  "workflowId": "550e8400-e29b-41d4-a716-446655440000",
  "inputs": {
    "user_id": "user-12345"
  }
}

Email + Phone Fraud Check:

{
  "workflowId": "550e8400-e29b-41d4-a716-446655440000",
  "inputs": {
    "user_id": "user-12345",
    "email": "[email protected]",
    "phone_number": "14155551234"
  }
}

Face Match with Reference Image:

{
  "workflowId": "550e8400-e29b-41d4-a716-446655440000",
  "inputs": {
    "user_id": "user-12345",
    "reference_image": "https://example.com/photos/user-12345.jpg"
  }
}

NIN eKYC (Nigeria):

{
  "workflowId": "550e8400-e29b-41d4-a716-446655440000",
  "inputs": {
    "user_id": "user-12345",
    "user_firstname": "Adebayo",
    "user_lastname": "Okonkwo",
    "user_dob": "1985-03-22",
    "nin": "12345678901"
  }
}

Full KYC Flow:

{
  "workflowId": "550e8400-e29b-41d4-a716-446655440000",
  "inputs": {
    "user_id": "user-12345",
    "email": "[email protected]",
    "phone_number": "14155551234",
    "user_fullname": "John Michael Doe",
    "user_dob": "1990-05-15",
    "user_country": "US"
  }
}

1.7 Webhooks

Webhooks allow your backend to receive real-time notifications when verification sessions complete or change status. This enables you to update user records, trigger downstream processes, or notify users without polling the API.

Webhook Events

| Event | Description | | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | idv/session_finished | Fired when a document verification, selfie verification, or address verification check completes (approved, review, or declined) | | orchestration/workflow_execution_finished | Fired when an orchestration workflow execution completes | | orchestration/workflow_execution_updated | Fired when an orchestration workflow execution status is updated | | transaction/processed | Fired after each step of the orchestration workflow execution |

Webhook Configuration

Configure webhooks in the Admin Panel:

📍 Admin Panel path: Settings → Webhooks → Add Webhook

| Setting | Description | | ---------- | -------------------------------------------------------- | | URL | Your HTTPS endpoint to receive webhook events | | Events | Select which events to subscribe to | | Secret | Signing secret for payload verification (auto-generated) |

⚠️ Security: Your webhook endpoint must use HTTPS. HTTP endpoints are not supported.

Webhook Payload Structure

Headers:

Content-Type: application/json
SEON-Signature: <hmac_signature>
Request-Id: <uuid>
Webhook-Schema-Version: v1
User-Agent: seon/2.0.0

Base Payload:

interface WebhookPayload {
  event: string; // Event type (e.g., 'idv/session_finished')
  timestamp: string; // ISO 8601 timestamp when webhook was triggered
  data: WebhookData; // Event-specific payload data
}

IDV Check (Document Verification, Selfie Verification, Address Verification) Webhook Data

For idv/session_finished event:

interface IdvWebhookData {
  // Session metadata
  status: 'APPROVED' | 'REVIEW' | 'DECLINED';
  statusDetail?: string; // Additional context for the status
  platform: 'IOS' | 'ANDROID' | 'WEB';
  duplicatesFound: boolean; // Whether duplicate sessions were detected
  startedAt: string; // ISO 8601 timestamp
  finishedAt: string; // ISO 8601 timestamp
  sessionId: string; // UUID - unique session identifier
  templateId: string; // ID of the template used
  referenceId: string; // Reference ID provided at session start

  // User-provided fields (from session init)
  userId?: string;
  email?: string;
  name?: string;
  referenceDateOfBirth?: { day: number; month: number; year: number };
  postalCode?: string;
  additionalProperties?: Record<string, string>;

  // Check results (present when respective checks were performed)
  documentCheckResult?: DocumentCheckResult;
  dataExtractionResult?: DataExtractionResult; // Deprecated: use documentCheckExtractedData
  documentCheckExtractedData?: DataExtractionResult;
  selfieVerificationResult?: SelfieVerificationResult;
  proofOfAddressCheckResult?: ProofOfAddressCheckResult;
  proofOfAddressExtractedData?: ProofOfAddressExtractionResult;
  capturedMedia?: CapturedMedia;
}

Orchestration Workflow Webhook Data

For orchestration/workflow_execution_finished and orchestration/workflow_execution_updated events:

interface OrchestrationWebhookData {
  id: string; // Workflow execution ID (UUID)
  workflow: {
    id: string; // Workflow definition ID (UUID)
    name: string; // Workflow name
  };
  status: OrchestrationStatus;
}

type OrchestrationStatus =
  | 'CREATED' // Execution created, not yet started
  | 'RUNNING' // Execution in progress
  | 'APPROVED' // Completed successfully
  | 'REVIEW' // Requires manual review
  | 'DECLINED' // Failed/rejected
  | 'EXPIRED' // Session timed out
  | 'ERROR'; // System error occurred

Example:

{
  "event": "orchestration/workflow_execution_finished",
  "timestamp": "2025-02-03T09:11:39.000Z",
  "data": {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "workflow": {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "KYC Onboarding Flow"
    },
    "status": "APPROVED"
  }
}

💡 Note: To get full execution details (node results, IDV data, etc.), query the Admin API using the execution id from the webhook.

Document Check Result

type CheckResultValue = 'PASS' | 'REVIEW' | 'FAIL' | 'NOT_PERFORMED';
type OverallResultValue = 'APPROVED' | 'REVIEW' | 'DECLINED' | 'ABANDONED';

interface DocumentCheckResult {
  overallResult: OverallResultValue; // Aggregate result
  matchCheckResult?: CheckResultValue; // Document fields match MRZ/barcode
  formatCheckResult?: CheckResultValue; // Document format validity
  logicCheckResult?: CheckResultValue; // Logical consistency of data
  barcodeAnomalyCheckResult?: CheckResultValue;
  suspiciousDataCheckResult?: CheckResultValue;
  dataIntegrityCheckResult?: CheckResultValue;
  screenCheckResult?: CheckResultValue; // Screen recapture detection
  photocopyCheckResult?: CheckResultValue; // Photocopy detection
  handPresenceCheckResult?: CheckResultValue; // Hand holding document
  photoForgeryCheckResult?: CheckResultValue; // Photo manipulation detection
  securityFeaturesCheckResult?: CheckResultValue;
  documentValidityCheckResult?: CheckResultValue;
  imageQualityCheckResult?: CheckResultValue;
  // Reference data matching (when provided at session start)
  ageVerificationCheckResult?: CheckResultValue;
  nameMatchCheckResult?: CheckResultValue;
  dateOfBirthCheckResult?: CheckResultValue;
  postalCodeCheckResult?: CheckResultValue;
  stateCheckResult?: CheckResultValue;
}

| Check | Description | | ----------------------------- | ---------------------------------------------------- | | matchCheckResult | MRZ/barcode data matches visual zone | | formatCheckResult | Document layout matches expected format | | logicCheckResult | Data is logically consistent (e.g., age matches DOB) | | barcodeAnomalyCheckResult | Barcode structure and data validity | | suspiciousDataCheckResult | Known fraudulent patterns detected | | dataIntegrityCheckResult | Data hasn't been tampered with | | screenCheckResult | Document is not displayed on a screen | | photocopyCheckResult | Document is not a photocopy | | handPresenceCheckResult | Document is physically held | | photoForgeryCheckResult | Photo hasn't been digitally manipulated | | securityFeaturesCheckResult | Hologram, watermarks, etc. present | | documentValidityCheckResult | Document is not expired | | imageQualityCheckResult | Image resolution and clarity | | ageVerificationCheckResult | Age matches reference data | | nameMatchCheckResult | Name matches reference data | | dateOfBirthCheckResult | DOB matches reference data | | postalCodeCheckResult | Postal code matches reference data | | stateCheckResult | State matches reference data |

Data Extraction Result

interface DataExtractionResult {
  fullName?: string;
  birthDate?: string | null; // Format: YYYY-MM-DD
  age?: number | null; // Age in years
  gender?: string | null; // e.g., 'MALE', 'FEMALE'
  documentType?: 'NATIONAL_ID' | 'DRIVERS_LICENSE' | 'PASSPORT' | 'RESIDENT_PERMIT';
  documentNumber?: string | null;
  documentIssueDate?: string | null; // Format: YYYY-MM-DD
  documentExpirationDate?: string | null; // Format: YYYY-MM-DD
  country?: string; // ISO 3166-1 alpha-2 code
  address?: string | null; // Full address extracted from document
  personalIdNumber?: string; // Personal ID number (if present)
  postalCode?: string; // Postal code extracted from document
  state?: string; // State/province (ISO 3166-2 format)
}

Selfie Verification Result

interface SelfieVerificationResult {
  overallResult: OverallResultValue; // Aggregate result
  livenessCheckResult?: CheckResultValue; // Liveness detection passed
  faceMatchingResult?: CheckResultValue; // Face matches document photo
}

| Check | Description | | --------------------- | ------------------------------------- | | livenessCheckResult | User is a live person (anti-spoofing) | | faceMatchingResult | Selfie matches the document photo |

Proof of Address Check Result

interface ProofOfAddressCheckResult {
  overallResult: OverallResultValue;
  addressDocumentCheckResult?: CheckResultValue;
  addressDocumentMustNotBeExpiredCheckResult?: CheckResultValue;
  addressValidationCheckResult?: CheckResultValue;
  fullAddressCheckResult?: CheckResultValue;
  nameMatchCheckResult?: CheckResultValue;
}

interface ProofOfAddressExtractionResult {
  extractedAddress?: string;
}

Captured Media

interface CapturedMedia {
  documentFront?: string; // URL to front of document image
  documentBack?: string; // URL to back of document image
  selfieVideo?: string; // URL to selfie video
  proofOfAddress?: string; // URL to PoA document image
}

⚠️ Note: Media URLs are time-limited signed URLs. Download the media promptly after receiving the webhook.

Example Webhook Payloads

IDV Session — Approved:

{
  "event": "idv/session_finished",
  "timestamp": "2025-07-10T14:30:00Z",
  "data": {
    "status": "APPROVED",
    "statusDetail": "Verification process completed successfully.",
    "platform": "WEB",
    "duplicatesFound": false,
    "startedAt": "2025-07-10T14:25:10Z",
    "finishedAt": "2025-07-10T14:29:55Z",
    "sessionId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
    "templateId": "tpl_a1b2c3d4e5f67890",
    "referenceId": "user-xyz-123",
    "userId": "98765",
    "name": "Jane Doe",
    "referenceDateOfBirth": { "day": 15, "month": 5, "year": 1990 },
    "documentCheckResult": {
      "overallResult": "APPROVED",
      "matchCheckResult": "PASS",
      "logicCheckResult": "PASS",
      "formatCheckResult": "PASS",
      "screenCheckResult": "PASS",
      "photocopyCheckResult": "PASS",
      "handPresenceCheckResult": "PASS",
      "photoForgeryCheckResult": "PASS",
      "securityFeaturesCheckResult": "PASS",
      "documentValidityCheckResult": "PASS",
      "imageQualityCheckResult": "PASS",
      "ageVerificationCheckResult": "PASS",
      "nameMatchCheckResult": "PASS",
      "dateOfBirthCheckResult": "PASS"
    },
    "dataExtractionResult": {
      "fullName": "JANE DOE",
      "birthDate": "1990-05-15",
      "age": 35,
      "gender": "FEMALE",
      "documentType": "NATIONAL_ID",
      "documentNumber": "ID12345678",
      "documentIssueDate": "2020-01-20",
      "documentExpirationDate": "2030-01-19",
      "country": "HU",
      "address": "123 EXAMPLE STREET, BUDAPEST"
    },
    "selfieVerificationResult": {
      "overallResult": "APPROVED",
      "livenessCheckResult": "PASS",
      "faceMatchingResult": "PASS"
    },
    "capturedMedia": {
      "documentFront": "https://media.seon.io/doc_front_a1b2.jpg",
      "documentBack": "https://media.seon.io/doc_back_c3d4.jpg",
      "selfieVideo": "https://media.seon.io/selfie_e5f6.mp4"
    }
  }
}

IDV Session — Review Required:

{
  "event": "idv/session_finished",
  "timestamp": "2025-07-10T15:05:12Z",
  "data": {
    "status": "REVIEW",
    "statusDetail": "Document image quality is too low for automated processing.",
    "platform": "IOS",
    "duplicatesFound": false,
    "startedAt": "2025-07-10T15:01:00Z",
    "finishedAt": "2025-07-10T15:04:45Z",
    "sessionId": "f0e9d8c7-b6a5-4321-fedc-ba9876543210",
    "templateId": "tpl_f0e9d8c7b6a54321",
    "referenceId": "user-abc-456",
    "userId": "54321",
    "email": "[email protected]",
    "documentCheckResult": {
      "overallResult": "REVIEW",
      "matchCheckResult": "REVIEW",
      "formatCheckResult": "REVIEW",
      "securityFeaturesCheckResult": "REVIEW",
      "imageQualityCheckResult": "FAIL"
    },
    "dataExtractionResult": {
      "fullName": "JOHN SMITH",
      "birthDate": null,
      "documentType": "DRIVERS_LICENSE",
      "documentExpirationDate": "2028-11-10",
      "country": "US"
    },
    "selfieVerificationResult": {
      "overallResult": "APPROVED",
      "livenessCheckResult": "PASS",
      "faceMatchingResult": "PASS"
    },
    "capturedMedia": {
      "documentFront": "https://media.seon.io/doc_front_f0e9.jpg",
      "selfieVideo": "https://media.seon.io/selfie_d8c7.mp4"
    }
  }
}

Signature Verification

Verify webhook authenticity by validating the HMAC signature in the SEON-Signature header:

Node.js:

const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signature, secret) {
  const expectedSignature = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');

  return crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(signature));
}

// Express middleware example
app.post('/webhook/seon', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['seon-signature'];
  const requestId = req.headers['request-id'];

  if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Parse the verified payload
  const payload = JSON.parse(req.body);
  const { event, timestamp, data } = payload;

  console.log(`[${requestId}] Event ${event}: ${data.status}`);

  res.status(200).json({ received: true });
});

Python:

import hmac
import hashlib
import os
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('SEON_WEBHOOK_SECRET')

def verify_signature(raw_body, signature, secret):
    expected = hmac.new(
        secret.encode(),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route('/webhook/seon', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('SEON-Signature')
    request_id = request.headers.get('Request-Id')

    if not verify_signature(request.data, signature, WEBHOOK_SECRET):
        return jsonify({'error': 'Invalid signature'}), 401

    # Process webhook
    payload = request.json
    event = payload['event']
    data = payload['data']

    print(f"[{request_id}] Event {event}: {data.get('status')}")

    return jsonify({'received': True}), 200

Retry Logic

SEON retries failed webhook deliveries with exponential backoff:

| Attempt | Delay | Total Time Elapsed | | :-----: | ---------- | :----------------: | | 1 | Immediate | 0 | | 2 | 1 minute | 1 min | | 3 | 5 minutes | 6 min | | 4 | 30 minutes | 36 min | | 5 | 2 hours | 2h 36min | | 6 | 24 hours | ~26h 36min |

Success Criteria:

  • HTTP status 2xx = Success (no retry)
  • HTTP status 4xx = Client error (no retry, except 429)
  • HTTP status 5xx = Server error (will retry)
  • Timeout (30 seconds) = Will retry

💡 Best Practice: Return 200 OK immediately after receiving the webhook, then process asynchronously. This prevents timeout issues for long-running operations.

Handling Webhooks Idempotently

Webhooks may be delivered more than once. Use sessionId (for IDV) or id (for orchestration) as an idempotency key:

app.post('/webhook/seon', async (req, res) => {
  const { event, data } = req.body;
  const idempotencyKey = data.sessionId || data.id; // sessionId for IDV, id for orchestration

  // Check if already processed
  const existing = await db.webhookEvents.findOne({ idempotencyKey, event });
  if (existing) {
    return res.status(200).json({ received: true, duplicate: true });
  }

  // Store and process
  await db.webhookEvents.insert({ idempotencyKey, event, data, processedAt: new Date() });
  await processVerificationResult(data);

  res.status(200).json({ received: true });
});

1.8 Integration Examples

Backend: Initialize Workflow and Return Token

Node.js (Express)
const express = require('express');
const app = express();
app.use(express.json());

const SEON_API_KEY = process.env.SEON_API_KEY;
const SEON_BASE_URL = process.env.SEON_BASE_URL; // e.g., https://orchestration.seon.io
const WORKFLOW_ID = process.env.WORKFLOW_ID;

app.post('/api/init-verification', async (req, res) => {
  const { userId, email, phoneNumber } = req.body;

  try {
    const response = await fetch(`${SEON_BASE_URL}/v1/init-workflow`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': SEON_API_KEY,
      },
      body: JSON.stringify({
        workflowId: WORKFLOW_ID,
        inputs: {
          user_id: userId,
          email: email, // Required if Email check is enabled
          phone_number: phoneNumber, // Required if Phone check is enabled
        },
      }),
    });

    if (!response.ok) {
      const error = await response.json();
      return res.status(response.status).json({ error: error.message });
    }

    const { data } = await response.json();
    res.json({ token: data.token });
  } catch (error) {
    console.error('Failed to initialize workflow:', error);
    res.status(500).json({ error: 'Failed to initialize verification' });
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));
Java (Spring Boot)
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
import java.util.HashMap;

@RestController
@RequestMapping("/api")
public class VerificationController {

    @Value("${seon.api.key}")
    private String seonApiKey;

    @Value("${seon.base.url}")
    private String seonBaseUrl;

    @Value("${seon.workflow.id}")
    private String workflowId;

    private final RestTemplate restTemplate = new RestTemplate();

    @PostMapping("/init-verification")
    public ResponseEntity<?> initVerification(@RequestBody InitRequest request) {
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.set("x-api-key", seonApiKey);

            Map<String, Object> inputs = new HashMap<>();
            inputs.put("user_id", request.getUserId());
            if (request.getEmail() != null) {
                inputs.put("email", request.getEmail());
            }
            if (request.getPhoneNumber() != null) {
                inputs.put("phone_number", request.getPhoneNumber());
            }

            Map<String, Object> body = new HashMap<>();
            body.put("workflowId", workflowId);
            body.put("inputs", inputs);

            HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);

            ResponseEntity<Map> response = restTemplate.postForEntity(
                seonBaseUrl + "/v1/init-workflow",
                entity,
                Map.class
            );

            Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
            return ResponseEntity.ok(Map.of("token", data.get("token")));

        } catch (Exception e) {
            return ResponseEntity.status(500)
                .body(Map.of("error", "Failed to initialize verification"));
        }
    }
}

// Request DTO
class InitRequest {
    private String userId;
    private String email;
    private String phoneNumber;

    // Getters and setters
    public String getUserId() { return userId; }
    public void setUserId(String userId) { this.userId = userId; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getPhoneNumber() { return phoneNumber; }
    public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; }
}
Python (Flask)
import os
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

SEON_API_KEY = os.environ.get('SEON_API_KEY')
SEON_BASE_URL = os.environ.get('SEON_BASE_URL')  # e.g., https://orchestration.seon.io
WORKFLOW_ID = os.environ.get('WORKFLOW_ID')

@app.route('/api/init-verification', methods=['POST'])
def init_verification():
    data = request.get_json()
    user_id = data.get('userId')
    email = data.get('email')
    phone_number = data.get('phoneNumber')

    try:
        response = requests.post(
            f'{SEON_BASE_URL}/v1/init-workflow',
            headers={
                'Content-Type': 'application/json',
                'x-api-key': SEON_API_KEY,
            },
            json={
                'workflowId': WORKFLOW_ID,
                'inputs': {
                    'user_id': user_id,
                    'email': email,           # Required if Email check is enabled
                    'phone_number': phone_number,  # Required if Phone check is enabled
                },
            },
        )

        if not response.ok:
            return jsonify({'error': response.json().get('message')}), response.status_code

        token = response.json()['data']['token']
        return jsonify({'token': token})

    except Exception as e:
        print(f'Failed to initialize workflow: {e}')
        return jsonify({'error': 'Failed to initialize verification'}), 500

if __name__ == '__main__':
    app.run(port=3000)

Frontend: Start SDK with Token

React
import { useEffect, useState } from 'react';
import SeonOrchestration from '@seontechnologies/seon-orchestration';

function VerificationButton({ userId, onComplete, onError }) {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Set up event listeners
    const handleCompleted = (status) => {
      setIsLoading(false);
      onComplete(status); // 'success', 'pending', 'failed', or 'unknown'
    };

    const handleError = (errorCode) => {
      setIsLoading(false);
      setError(`Verification error: ${errorCode}`);
      onError(errorCode);
    };

    const handleClosed = () => {
      setIsLoading(false);
    };

    SeonOrchestration.on('completed', handleCompleted);
    SeonOrchestration.on('error', handleError);
    SeonOrchestration.on('closed', handleClosed);

    return () => {
      SeonOrchestration.off('completed', handleCompleted);
      SeonOrchestration.off('error', handleError);
      SeonOrchestration.off('closed', handleClosed);
    };
  }, [onComplete, onError]);

  const startVerification = async () => {
    setIsLoading(true);
    setError(null);

    try {
      // 1. Get token from your backend
      const response = await fetch('/api/init-verification', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId }),
      });

      if (!response.ok) {
        throw new Error('Failed to initialize verification');
      }

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

      // 2. Start the SDK with the token
      await SeonOrchestration.start({
        token,
        language: 'en',
        renderingMode: 'popup', // or 'fullscreen', 'inline'
      });
    } catch (err) {
      setError(err.message);
      setIsLoading(false);
    }
  };

  return (
    <div>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <button onClick={startVerification} disabled={isLoading}>
        {isLoading ? 'Loading...' : 'Start Verification'}
      </button>
    </div>
  );
}

export default VerificationButton;
Vanilla JavaScript

Note: The SDK is only available as an npm package. Use a bundler (Webpack, Vite, Rollup, etc.) to include it in your project.

import { SeonOrchestration } from '@seontechnologies/seon-orchestration';

// Set up event listeners
SeonOrchestration.on('completed', (status) => {
  console.log('Verification completed:', status);
  // Handle: 'success', 'pending', 'failed', or 'unknown'
});

SeonOrchestration.on('error', (errorCode) => {
  console.error('Verification error:', errorCode);
});

async function startVerification(userId) {
  try {
    // 1. Get token from your backend
    const response = await fetch('/api/init-verification', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ userId }),
    });

    if (!response.ok) {
      throw new Error('Failed to initialize verification');
    }

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

    // 2. Start the SDK
    await SeonOrchestration.start({
      token,
      language: 'en',
      renderingMode: 'popup',
    });
  } catch (error) {
    console.error('Failed to start verification:', error);
  }
}

// Attach to button
document.getElementById('start-btn').addEventListener('click', () => {
  startVerification('user-12345');
});
<!-- In your HTML -->
<button id="start-btn">Start Verification</button>

2. License Terms

The detailed license terms can be viewed at the following link: ®️SEON License Terms.