The Verifa JavaScript SDK handles the frontend capture experience — opening the verification UI as a popup, modal, or redirect. Your backend creates the session, the SDK handles the rest.
Verifa exposes the same capabilities through two equivalent JavaScript APIs:
Both call the same underlying code path. Pick one and stay consistent. The rest
of this page uses the flat Verifa.open() form because it’s been around longer;
skip to the Verifa.Client reference for the new form.
The most secure and production-ready approach: your backend creates the session using a secret API key, immediately exchanges it for a short-lived embed token, and passes ONLY that token’s URL to the frontend. The long-lived capture URL never leaves your server.
This matches the pattern used by Persona (inquiry session token), Stripe
Identity (client_secret), and Plaid Link (link_token).
Or via npm:
The widget loads the capture flow inside an <iframe> on your page. For the
iframe to access the camera and render correctly, your page must permit it.
Most customers don’t set the relevant headers at all — in which case
browser defaults apply and everything works out of the box. If your page does
set Content-Security-Policy or Permissions-Policy, you must explicitly
allow Verifa’s origin.
Delegate camera and microphone to Verifa’s origin (the iframe’s allow=
attribute alone is not sufficient — the host’s Permissions-Policy is the gate):
If your headers block the iframe, the modal opens but the camera permission
prompt never appears and the capture page stays blank. The SDK detects this
case and emits onError({ code: "ready_timeout", message: "..." }) after
8 seconds (configurable via readyTimeoutMs). Check your browser’s
developer console for messages mentioning “Content Security Policy”,
“Permissions-Policy”, or “frame-ancestors” — those will pinpoint the
exact header that needs to change.
Two API calls, both with the secret API key:
The embed_url and client_token both expire in 30 minutes. The long-lived
session capture URL (session.capture_url) lives 24 hours, but you don’t
need to expose it to the browser — that’s the whole point of this pattern.
If a user takes a long time to finish, re-issue the client token on your
backend before it expires.
Equivalent using the Persona-shaped sessionToken form:
Both produce the same iframe URL — pick whichever fits your codebase.
Results contain PII and must be retrieved server-side:
Or receive results via webhook (recommended for production).
Verifa.Client class APIClass-based API matching the Persona-style surface. Construct an instance with
your options, then call .open(). The instance manages lifecycle and emits
callbacks for each verification event.
Verifa.open(options) — flat / compat formEquivalent to new Verifa.Client(...).open() but returns a Promise. Useful
for await style or existing integrations.
Returns: Promise<{ sessionId: string, status: string }>
Opens the capture UI in a centered popup window. Best for desktop.
If the browser blocks the popup, onError fires. Fall back to modal mode.
Opens the capture UI in a full-screen overlay with an iframe. Works on desktop and mobile. Closes with the X button, backdrop click, or Escape key.
Navigates the browser to the capture page. After completion, the user returns
to the redirect_url you specified when creating the session.
The user lands back on your redirect URL with query parameters:
The SDK only runs in the browser. Mark the component "use client" and load
the bundle with next/script so the integrity hash + crossorigin are set
correctly.
The session-create call (/api/create-verification) lives in a Next.js
Route Handler
or Server Action so the secret key never reaches the browser bundle.
onError is called with { code, message } for every recoverable and
unrecoverable failure. Branch on code for stable handling — message may
change between versions.
onError always fires once and then the SDK tears down — onComplete will not
fire after an error. If you need both branches, mirror the logic:
Use sandbox API keys to test without real verifications. In sandbox mode, the capture flow lets you choose the outcome (approve, reject, or needs review).
getUserMedia inside a cross-origin iframe was unreliable on iOS Safari
prior to 16.4 (March 2023). On older devices the modal opens but the
camera prompt never appears, and the SDK fires
onError({ code: 'ready_timeout' }) after the timeout. Detect this and
offer the user a fallback:
mode: 'popup' requires .open() to be called from a synchronous click
handler. If you await a fetch before opening the popup, browsers will
block it. Two patterns work:
ready_timeout debuggingIf the modal opens but the camera prompt never appears and you get
ready_timeout, your host page is almost certainly blocking the iframe.
Check the browser console:
If nothing shows in the console, the iframe loaded fine but the capture
JavaScript itself failed to boot. Inspect the iframe’s network tab for a
401/403 on /verify/* — that means the capture URL has expired or the
session was already submitted.
When the modal is open and the user switches tabs, the camera stream pauses (browser-level behavior). The capture flow resumes when the tab is focused again. No SDK action needed.
Content-Security-Policy: ...; sandbox)Pages with a strict sandbox directive can’t open child iframes at all. If
you must run with sandbox, use mode: 'redirect' — the user leaves your
page, completes verification on app.withverifa.com, and returns via
redirectUrl.
The SDK uses fetch, Promise, and postMessage, supported in all modern
browsers:
No polyfills required for any browser released after 2017.
captureUrl flow, the
secret key stays on your server. The frontend only receives a capture URL
scoped to one session, which expires when the session does (default 24 h,
configurable per-session).vk_pub_*). Publishable keys are required to declare an
Allowed Origins list; requests from any other origin are rejected with 403.
See Authentication → Publishable keys.integrity= hash so
a tampered script can’t execute. The browser refuses the script if the hash
doesn’t match the bytes received.