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.tsdeclarations - 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_…orpk_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
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.
import { OthentoSDK, OthentoError } from '@othento/web-sdk';
import type { OthentoConfig, OthentoDecision } from '@othento/web-sdk';Quick start
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' }). |
// 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".
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 clearedCallbacks 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
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. |
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:
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
Modal (default)
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.
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. |
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
export class VerifyComponent implements OnDestroy {
private sdk = new OthentoSDK({ /* … */ });
ngOnInit() { this.sdk.start(); }
ngOnDestroy() { this.sdk.destroy(); }
}React
useEffect(() => {
const sdk = new OthentoSDK({ /* … */ });
sdk.start();
return () => sdk.destroy();
}, []);Vue
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.
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:
onReadyfires within ~1 s ofstart()on a healthy network.onSessionCreatedfires next, with a non-emptyexternalId.onStatusChangefires at least once with'InProgress'.- Exactly one terminal callback fires:
onCompleted,onCancelled, oronError.
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
-
onReadyandonSessionCreatedfire - 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.
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
postMessagebridge verifies the origin on every inbound and outbound message — messages from unknown origins are dropped. - The bridge is versioned (
v: 1). Protocol mismatches surface asonError({ 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), theexternalIdof the affected session, and the browser console log.
License
UNLICENSED — use under your Othento partner agreement. See CHANGELOG.md for release history.