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

1.1 Getting Started
Installation
npm install @seontechnologies/seon-orchestration
# or
yarn add @seontechnologies/seon-orchestrationPrerequisites
- 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
lightanddarkobjects 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
languageis 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' });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) andinputs.user_idare 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
- Navigate to Transactions → Workflow Runs
- Search by
user_idorexecutionId - 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/jsonRequest 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_fullnameis 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.0Base 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 occurredExample:
{
"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
idfrom 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}), 200Retry 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, except429) - HTTP status
5xx= Server error (will retry) - Timeout (30 seconds) = Will retry
💡 Best Practice: Return
200 OKimmediately 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.
