Skip to content
SDKs

Web SDK

Embed the Othento verification flow in any web app. The package is framework-agnostic, about 3.6 KB gzipped with zero runtime dependencies, and ships ESM, CommonJS and TypeScript types.

Drop-in identity verification for the web — document capture, OCR, face match, and liveness — embedded in your app with a few lines of JavaScript.

The SDK mounts an isolated cross-origin iframe in modal or inline mode, drives it over a versioned postMessage bridge (origin-checked, never a wildcard target), and hands you a typed decision (Approved / Declined / InReview) when the verification finishes. The API surface mirrors the Android SDK 1:1 so error codes, session statuses, and analytics events line up across platforms.

  • Bundle size: ~3.6 KB gzipped, zero runtime dependencies
  • Distribution: ESM + CJS + .d.ts declarations
  • Frameworks: framework-agnostic — works in plain HTML, React, Angular, Vue, Svelte, Next.js
  • Compliance: PCI-/SOC 2-conscious — no PII or biometric data ever crosses the host page


Requirements

Before you wire the SDK in, make sure you have:

  • A partner account on the Othento platform with at least one workflow configured. Workflow IDs are issued from your dashboard.
  • A public API key (pk_sandbox_… or pk_live_…) — sandbox keys hit the test environment, live keys bill against your plan. You do not pass an environment flag; the key decides. See Sandbox vs production.
  • An HTTPS hosting page (the camera API is gated to secure contexts).
  • A user-facing camera.

If you don't have credentials yet, contact your account manager to be onboarded.


Install

Shell
npm install @othento/web-sdk
# or
pnpm add @othento/web-sdk
# or
yarn add @othento/web-sdk

The package ships ESM (.mjs), CommonJS (.cjs), and TypeScript declarations (.d.ts). Tree-shakers honour sideEffects: false.

TypeScript
import { OthentoSDK, OthentoError } from '@othento/web-sdk';
import type { OthentoConfig, OthentoDecision } from '@othento/web-sdk';

Quick start

TypeScript
import { OthentoSDK } from '@othento/web-sdk';

const sdk = new OthentoSDK({
  mode: 'create',
  apiKey: '<<YOUR_PUBLIC_API_KEY>>',
  workflowExternalId: '<<YOUR_WORKFLOW_ID>>',
  clientData: 'user-123', // your end-user identifier

  onReady: () => console.log('SDK ready'),
  onSessionCreated: ({ externalId }) => trackSession(externalId),
  onStatusChange: (status) => console.log('status:', status),
  onCompleted: ({ decision, externalId }) =>
    finishOnboarding(decision, externalId),
  onCancelled: ({ reason }) => analytics.track('verify_cancel', { reason }),
  onError: (err, displayMessage) => {
    console.error(err.code, displayMessage);
  },
});

sdk.start();

Configuration is validated synchronously. Missing or malformed fields throw OthentoError immediately — they do not surface via onError.


Configuration reference

new OthentoSDK(config) takes one argument: an OthentoConfig object.

Authentication

Field Type Required Notes
mode 'create' always Always 'create'.
apiKey string always Public API key issued by your dashboard.
workflowExternalId string always Verification workflow identifier.
clientData string always Your end-user identifier; echoed in session events and webhooks.

Session metadata

Field Type Required Notes
callbackUrl string optional Forwarded to create-session. Where the user is redirected on completion (if applicable).
callbackReceiver 'Initiator' | 'Completer' | 'Both' optional Which party receives the callbackUrl redirect.
metadata string optional Free-form string round-tripped on session events and webhooks.
expectedDetails OthentoExpectedDetails optional Pre-filled identity hints to compare against extracted data.

Presentation

Field Type Required Default Notes
container HTMLElement | string optional (modal) If set, renders inline inside the element (or CSS selector match). See Presentation.
showCloseButton boolean optional true Modal only. Hide the built-in × button if your UI provides its own dismissal.
closeOnBackdropClick boolean optional false Modal only. Treat backdrop clicks as cancellations.

Network & timeouts

The SDK guards startup with a two-stage timeout: the iframe must fire its load event within loadTimeoutMs, then complete the ready handshake within initTimeoutMs (measured from load). Both are validated as positive numbers at construction. Raise them when your end-users are on slow or unreliable networks where the first cold load of the verification UI can exceed the defaults.

Field Type Required Default Notes
loadTimeoutMs number optional 20000 (20s) Time allowed for the iframe load event before onError({ code: 'iframe_load_failed' }).
initTimeoutMs number optional 15000 (15s) Time allowed (after load) for the ready handshake before onError({ code: 'init_timeout' }).
TypeScript
// Example: generous limits for users on weak mobile connections.
const sdk = new OthentoSDK({
  mode: 'create',
  apiKey: 'pk_live_…',
  workflowExternalId: 'wf_…',
  clientData: 'user-123',
  loadTimeoutMs: 60000, // wait up to 60s for the iframe to load
  initTimeoutMs: 30000, // then up to 30s for the handshake
});

OthentoExpectedDetails

Every field is optional, so include only the values you already know. The backend compares each one with the data read from the document and selfie. Format matters: a correct value in the wrong format won't match. For example, "01/01/1988" won't match a date of birth and "Jordan" won't match a country, because they need to be "1988-01-01" and "JOR".

TypeScript
const sdk = new OthentoSDK({
  mode: 'create',
  apiKey: 'pk_live_…',
  workflowExternalId: 'wf_…',
  clientData: 'user-123',
  expectedDetails: {
    firstName: 'Ahmad',
    lastName: 'Khalil',
    dateOfBirth: '1988-01-01', // yyyy-MM-dd
    gender: 'M',               // 'M' or 'F'
    nationality: 'JOR',        // ISO 3166-1 alpha-3
    country: 'JOR',            // ISO 3166-1 alpha-3
    address: 'Amman, Jordan',
    documentNumber: 'A1234567',
    ipAddress: '203.0.113.24', // end user's public IPv4
  },
});
Field Type Expected format Example
firstName string First (given) name, spelled as on the identity document. "Ahmad"
lastName string Last (family) name, spelled as on the identity document. "Khalil"
dateOfBirth string yyyy-MM-dd: a 4-digit year and a zero-padded 2-digit month and day. Don't add a time or timezone. "1988-01-01"
gender string A single uppercase letter, "M" or "F". Values like "Male" or "f" won't match. "M"
nationality string Nationality as an ISO 3166-1 alpha-3 code (3 uppercase letters). "JOR"
country string ISO 3166-1 alpha-3 code (3 uppercase letters). Don't use a country name or a 2-letter code. "JOR"
address string Address as free text. "Amman, Jordan"
documentNumber string Passport or national ID number as printed on the document. Don't add spaces or dashes of your own. "A1234567"
ipAddress string Public IPv4 address the end user's device should connect from, in dotted-decimal form. Don't use your server's IP. "203.0.113.24"

If you don't know a value, leave the property out (or set it to undefined). Don't send an empty string or a placeholder such as "N/A".

Sandbox vs production

There is no environment flag. Your API key (and the session minted from it) decides whether traffic hits sandbox or production. Use a sandbox key during integration; swap to a live key when you're ready to bill real verifications. Same SDK build, same iframe URL, same bridge protocol.


Lifecycle & callbacks

A single session runs through this lifecycle:

constructor → start() → onReady → onSessionCreated   ─┐
                            ↓                         │
                     onStatusChange (deduped, n×)     │
                            ↓                         │
                  exactly one terminal callback:      │
                  onCompleted | onCancelled | onError │
                            ↓                         │
                     iframe unmounted, listeners cleared

Callbacks are invoked on a microtask, so they will not run synchronously inside start(). Exceptions thrown from your callback are caught and logged — they do not crash the SDK.

Callback Signature When it fires
onReady () => void Iframe finished loading and completed the handshake. Fires once, before any other lifecycle event.
onSessionCreated (info: { externalId, sessionUrl }) => void A new session has been minted. Save externalId against your user record.
onStatusChange (status: OthentoSessionStatus) => void Session status transitioned. Deduplicated — you never see the same status twice in a row.
onCompleted (result: { decision, externalId }) => void Verification reached a terminal decision. decision is Approved, Declined, or InReview.
onCancelled (info: { reason: OthentoCancelReason }) => void User dismissed the flow, host called destroy(), page unloaded, or the iframe self-cancelled.
onError (error: OthentoError, displayMessage: string) => void Any non-decision terminal error. error.code is a typed OthentoErrorCode.

Exactly one of onCompleted / onCancelled / onError fires per session — they are mutually exclusive terminal events. After a terminal event the SDK is in destroyed state; further calls to destroy() are safe no-ops.


Status & decision values

TypeScript
type OthentoSessionStatus =
  | 'Created'
  | 'InProgress'
  | 'Processing'
  | 'Completed'
  | 'Expired'
  | 'Failed';

type OthentoDecision = 'Approved' | 'Declined' | 'InReview';

These strings are identical to the Android SDK's enum names, so the same analytics events and webhook payloads work across both platforms.


Error codes

OthentoError.code is a typed OthentoErrorCode union — switch on it for safe, exhaustive error handling. Code names match the Android SDK 1:1, plus web-only additions for iframe and CSP failures.

code Origin Meaning Recommended action
config_invalid Thrown sync Required field missing or malformed. Fix at build time — this should never reach production.
missing_api_key Thrown sync Missing apiKey. Provide the public API key from your dashboard.
missing_workflow Thrown sync Missing workflowExternalId. Provide the workflow ID configured in your dashboard.
missing_client_data Thrown sync Missing clientData. Pass your end-user identifier.
iframe_load_failed onError Iframe never loaded within loadTimeoutMs (default 20 s). Check network connectivity and that your CSP allows frame-src https://sdk.othento.com. For slow networks, raise loadTimeoutMs.
init_timeout onError Iframe loaded but never completed the ready handshake within initTimeoutMs (default 15 s). Likely a CSP frame-src or mixed-content block, or the host page itself runs inside a script-restricted sandbox. On slow networks, raise initTimeoutMs. See Troubleshooting.
version_mismatch onError Bridge protocol version mismatch. Upgrade the SDK to a version compatible with the iframe deployment.
camera_permission_denied onError User blocked the camera in the browser. Show a "grant camera and try again" prompt. This is the single largest drop-off cause in production.
session_expired onError The session was minted too long ago to start verification. Mint a new session and call start() on a fresh OthentoSDK instance.
network onError Transient network failure inside the verification flow. Retry with the same session.
create_failed onError The session could not be created. Verify apiKey and workflowExternalId; check the Othento status page.
initiate_failed onError The flow failed to start after the session was created. Retry on a fresh instance; if it persists, contact support.
documents_failed onError Document capture or processing failed inside the flow. Prompt the user to retry capture with better lighting and framing.
upload_failed onError Evidence upload failed. error.stage says which step. Retry; check connectivity. See upload stages below.
file_too_large onError A captured file exceeded the upload size limit. Prompt the user to retry capture.
poll_failed onError Polling for the verification result failed. Retry; the session may still resolve server-side.
session_not_found onError The session id did not resolve to a live session. Mint a fresh session and start a new instance.
session_failed onError The session ended in a failed state server-side. Mint a new session; inspect the session in your dashboard.
cancelled onError The flow was aborted in a way the iframe reports as an error. Treat as a cancellation and offer a retry.
unknown onError An unclassified runtime error. Show displayMessage; capture the console log and externalId for support.
TypeScript
onError: (err, displayMessage) => {
  switch (err.code) {
    case 'camera_permission_denied':
      showCameraHelp();
      break;
    case 'session_expired':
      restartSession();
      break;
    case 'iframe_load_failed':
    case 'init_timeout':
      showRetryWithSupportLink();
      break;
    case 'network':
      showRetry();
      break;
    default:
      showGenericError(displayMessage);
  }
};

OthentoError extends Error and carries code, message, and (where relevant) httpStatus and stage fields.

Upload stages

When error.code === 'upload_failed', error.stage is an OthentoUploadStage pinpointing which step of the evidence upload failed:

TypeScript
type OthentoUploadStage = 'UploadUrl' | 'S3Put' | 'ConfirmUpload';
stage Failed step
UploadUrl Requesting the pre-signed upload URL.
S3Put Uploading the file to object storage.
ConfirmUpload Confirming the completed upload with the backend.

Presentation: modal vs inline

The SDK renders as a centered modal over a backdrop and owns the chrome. The host page's <body> gains an overflow: hidden lock for the duration of the flow.

Inline

Pass a container to embed the SDK as a section of your own page. The host owns layout: no backdrop, no centering, no built-in close button.

TypeScript
new OthentoSDK({
  mode: 'create',
  apiKey: '<<YOUR_PUBLIC_API_KEY>>',
  workflowExternalId: '<<YOUR_WORKFLOW_ID>>',
  clientData: 'user-123',
  container: document.getElementById('verify-area')!, // or '#verify-area'
  onCompleted: ({ decision }) => console.log(decision),
}).start();

In inline mode, showCloseButton and closeOnBackdropClick are ignored. Size the container yourself — the iframe defaults to width: 100%; height: 100%; min-height: 560px to avoid collapsing inside a zero-height parent.

The container is resolved at start() time, not at construction, so it is safe to construct the SDK before the host element exists.


Cancellation reasons

onCancelled({ reason }) fires once with a typed reason. Useful for analytics funnels and adaptive retry UX.

reason Trigger
close-button User clicked the built-in × button. Hide it with showCloseButton: false.
backdrop User clicked the backdrop. Off by default; enable with closeOnBackdropClick: true.
esc User pressed Escape (modal mode only).
page-hide Host page was unloaded (pagehide event).
host-destroy Host called sdk.destroy() before a terminal event.
iframe Verification was cancelled from inside the iframe.
TypeScript
onCancelled: ({ reason }) => {
  analytics.track('othento_cancel', { reason });
  if (reason === 'iframe') showRetryPrompt();
};

Cleaning up in SPAs

If you embed the SDK inside an Angular/React/Vue route, call sdk.destroy() on route change / component unmount. Without it the iframe, postMessage listeners, and (in modal mode) the document body overflow lock persist after navigation.

Angular

TypeScript
export class VerifyComponent implements OnDestroy {
  private sdk = new OthentoSDK({ /* … */ });

  ngOnInit() { this.sdk.start(); }
  ngOnDestroy() { this.sdk.destroy(); }
}

React

TypeScript
useEffect(() => {
  const sdk = new OthentoSDK({ /* … */ });
  sdk.start();
  return () => sdk.destroy();
}, []);

Vue

TypeScript
const sdk = new OthentoSDK({ /* … */ });
onMounted(() => sdk.start());
onBeforeUnmount(() => sdk.destroy());

After a terminal callback (onCompleted / onCancelled / onError), destroy() is a safe no-op.

Multi-instance safety

Constructing a second OthentoSDK while one is already running throws synchronously with code: 'config_invalid'. Destroy the first instance before starting another.


Content Security Policy & permissions

The hosting page must allow the SDK iframe and grant it camera access.

Content-Security-Policy
frame-src https://sdk.othento.com;
# Permissions-Policy (if you set one)
camera=(self "https://sdk.othento.com")

The SDK mounts the iframe with allow="camera". The bridge enforces origin verification on both incoming and outgoing postMessage payloads.

If your CSP omits frame-src, the iframe will silently fail to load and the SDK fires onError({ code: 'iframe_load_failed' }) after loadTimeoutMs (20 seconds by default).


Testing your integration

A working integration should produce:

  1. onReady fires within ~1 s of start() on a healthy network.
  2. onSessionCreated fires next, with a non-empty externalId.
  3. onStatusChange fires at least once with 'InProgress'.
  4. Exactly one terminal callback fires: onCompleted, onCancelled, or onError.

In sandbox, use the test identity documents listed in the dashboard to exercise approved / declined / in-review paths deterministically.

Sandbox flow checklist

  • Sandbox API key in place; the iframe loads
  • onReady and onSessionCreated fire
  • Approved-path test document → onCompleted({ decision: 'Approved' })
  • Declined-path test document → onCompleted({ decision: 'Declined' })
  • In-review test document → onCompleted({ decision: 'InReview' })
  • Closing the modal mid-flow → onCancelled({ reason: 'close-button' })

Troubleshooting

Symptom Likely cause Fix
onError({ code: 'iframe_load_failed' }) after loadTimeoutMs (~20 s default) CSP frame-src does not allow https://sdk.othento.com, network blocks the host, or the cold load is slower than the timeout. Update CSP. Verify the iframe loads in DevTools → Network. On weak networks, raise loadTimeoutMs.
onError({ code: 'init_timeout' }) after initTimeoutMs (~15 s default) Iframe loaded but the bridge handshake never completed. Often a CSP block on the SDK origin, the host page is embedded in a script-restricted sandbox, or a slow device/network. Verify the SDK origin loads in DevTools → Network, and that the host page is not embedded in a sandboxed iframe without allow-scripts. On slow networks, raise initTimeoutMs.
onError({ code: 'camera_permission_denied' }) User denied camera permission, or the host page is served over http://. Serve over HTTPS. Show a recovery UI instructing the user to grant camera permission and retry.
Modal opens but stays blank Mixed content (HTTP host loading the HTTPS iframe), or permissions-policy blocks camera. Move host to HTTPS. Add camera to Permissions-Policy.
onCancelled({ reason: 'page-hide' }) immediately after start() The SPA route changed and unmounted the host component before the flow could open. Construct and start() the SDK only when the route component is stably mounted.
Constructor throws 'Another OthentoSDK instance is currently active' A previous instance was not destroyed (e.g. component re-mounted in dev mode under StrictMode). Call sdk.destroy() on unmount and guard against double construction.
Callbacks fire in the wrong order Almost never the SDK — callbacks are dispatched on a microtask. Check for synchronous throws inside them. Wrap callbacks in try/catch and log; an exception inside one callback does not prevent the next.

If the symptom is not listed here, capture the network HAR and the browser console and contact support.


TypeScript

The package ships first-class TypeScript declarations with the build. No @types/… install is needed.

TypeScript
import {
  OthentoSDK,
  OthentoError,
  type OthentoConfig,
  type OthentoDecision,
  type OthentoErrorCode,
  type OthentoSessionStatus,
  type OthentoCancelReason,
  type OthentoExpectedDetails,
  type OthentoCallbacks,
} from '@othento/web-sdk';

OthentoErrorCode is a union literal; switch over it for exhaustive handling, and TypeScript will flag missing cases when new codes are added in future major versions.


Browser support

Modern evergreen browsers:

Browser Minimum version
Chrome / Edge Latest 2 stable releases
Firefox Latest 2 stable releases
Safari (macOS) 15+
Safari (iOS) 14+
Chrome (Android) Android 7+

Camera access requires a secure context (HTTPS or localhost). The SDK will load on http:// but the verification flow will fail immediately with camera_permission_denied.


Versioning & changelog

This package follows Semantic Versioning:

  • Patch (x.y.Z): bug fixes, internal refactors. Safe to auto-update.
  • Minor (x.Y.0): new optional config, new callbacks, new error codes. Existing callbacks keep their signatures.
  • Major (X.0.0): breaking changes to public types or callback signatures. Migration notes ship in CHANGELOG.md.

Pre-1.0.0 releases (0.x.y) may make breaking changes in minor versions as the API stabilises. Pin an exact version in production until 1.0.0.


Security & integrity

  • The SDK loads its iframe from a fixed origin that ships in the bundle. Consumers cannot redirect it to a different host.
  • The postMessage bridge verifies the origin on every inbound and outbound message — messages from unknown origins are dropped.
  • The bridge is versioned (v: 1). Protocol mismatches surface as onError({ code: 'version_mismatch' }) rather than crashing or producing undefined behaviour.
  • PII, document images, and biometric data never cross the host page — they are captured and uploaded directly from the iframe to the Othento backend.
  • Public API keys are scoped: they can only mint sessions and read their own data.

Support

  • Documentation: the latest version of this README and the full API reference live alongside the package on the dashboard.
  • Status & incidents: check the Othento status page before opening a ticket.
  • Account & integration help: reach out to your account manager or open a ticket through the dashboard. Include the SDK version (package.json), the externalId of the affected session, and the browser console log.

License

UNLICENSED — use under your Othento partner agreement. See CHANGELOG.md for release history.