For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
  • Getting Started
    • Introduction
    • How Verifa Works
    • Quickstart
    • Choosing an Integration Method
  • Use Cases
    • KYC Onboarding
    • Age Verification
    • AML Compliance
    • Fraud Prevention
    • Marketplace Trust & Safety
  • Core Concepts
    • Overview
    • Sessions
    • Verifications & Checks
    • Workflows
    • Identities
    • Cases
    • Screening & Reports
    • Lists
  • Integration Guides
    • Overview
    • JavaScript SDK
    • Web Capture Flow
    • API-Only Integration
    • Mobile SDK
    • Webhooks Guide
    • MCP Server
    • Migrating from Persona
  • API Details
    • Overview
    • Authentication
    • Pagination
    • Rate Limiting
    • Versioning
    • Errors
    • Webhooks
    • Idempotency
    • Key Inflection
    • Data Access
    • Data Retention
  • Tutorials
    • Creating Your First Verification Session
    • Creating a Workflow
    • Receiving Webhooks & Validating Signatures
    • Handling Webhook Events
    • Custom Document Types & AI Extraction
  • Best Practices
    • Testing
    • Preventing Duplicates
    • Fraud Signals
    • Changelog
  • API Reference
On this page
  • Two API styles
  • Recommended integration
  • 1. Add the script
  • Required host-page headers
  • Content-Security-Policy
  • Permissions-Policy
  • Symptom of misconfiguration
  • 2. Create a session and issue a client token on your server
  • 3. Open the capture UI on your frontend
  • 4. Get the result
  • API reference
  • Verifa.Client class API
  • Constructor options
  • Methods
  • Verifa.open(options) — flat / compat form
  • Display modes
  • Popup (default)
  • Modal
  • Redirect
  • Framework examples
  • React
  • Next.js (app router)
  • Vue
  • Vanilla JavaScript
  • Error handling
  • Error code reference
  • Sandbox testing
  • Edge cases
  • Mobile Safari before iOS 16.4
  • Popup blockers
  • ready_timeout debugging
  • Hidden iframe / tab visibility
  • Strict CSP (Content-Security-Policy: ...; sandbox)
  • Browser support
  • Security
  • Related
Integration Guides

JavaScript SDK

Was this page helpful?
Previous

Web Capture Flow

Next
Built with

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.

Two API styles

Verifa exposes the same capabilities through two equivalent JavaScript APIs:

StyleRecommended forExample
new Verifa.Client({...})New integrations. Matches Persona-style class API.new Verifa.Client({ captureUrl, onComplete }).open()
Verifa.open({...})Existing integrations. Returns a Promise.await Verifa.open({ captureUrl })

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.

Recommended integration

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

1. Add the script

1<script
2 src="https://cdn.withverifa.com/dist/verifa-v1.0.0.js"
3 integrity="sha384-/2DZ+OBwe8NRgYq4X0/qpvx7e7s9CjFy0K7WZX4E5utplz4njxn3A0tNTXNsK4fm"
4 crossorigin="anonymous"
5></script>

Or via npm:

1npm install @verifa/sdk
1import Verifa from "@verifa/sdk";

Required host-page headers

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.

Content-Security-Policy

Content-Security-Policy:
frame-src https://app.withverifa.com;
script-src https://cdn.withverifa.com;
connect-src https://app.withverifa.com;

Permissions-Policy

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):

Permissions-Policy:
camera=(self "https://app.withverifa.com"),
microphone=(self "https://app.withverifa.com")

Symptom of misconfiguration

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.

2. Create a session and issue a client token on your server

Two API calls, both with the secret API key:

Python
Node
cURL
1import requests
2
3HEADERS = {"X-API-Key": "vk_live_your_secret_key"}
4
5# 1. Create the session
6session = requests.post(
7 "https://api.withverifa.com/api/v1/sessions",
8 headers=HEADERS,
9 json={"external_ref": user_id, "country": "US"},
10).json()
11
12# 2. Issue a 30-minute embed token for it
13token = requests.post(
14 f"https://api.withverifa.com/api/v1/sessions/{session['id']}/client-token",
15 headers=HEADERS,
16).json()
17
18# Pass ONLY these to your frontend — never session["capture_url"]:
19return {
20 "embed_url": token["embed_url"],
21 "client_token": token["client_token"], # if you want the SDK's sessionToken form
22 "session_id": session["id"],
23 "expires_at": token["expires_at"],
24}

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.

3. Open the capture UI on your frontend

1<button id="verify-btn">Verify Identity</button>
2
3<script>
4 document.getElementById('verify-btn').addEventListener('click', async () => {
5 // Call YOUR backend — it returns the short-lived embed URL, not the API key
6 const { embed_url, session_id } = await fetch('/api/start-verification', {
7 method: 'POST',
8 }).then(r => r.json());
9
10 new Verifa.Client({
11 captureUrl: embed_url, // simplest path — just use the URL
12 onComplete: ({ sessionId, status }) => console.log('done', sessionId, status),
13 onCancel: () => console.log('user closed'),
14 onError: ({ code, message }) => console.error(code, message),
15 }).open();
16 });
17</script>

Equivalent using the Persona-shaped sessionToken form:

1new Verifa.Client({
2 sessionToken: client_token,
3 sessionId: session_id,
4 apiBase: 'https://api.withverifa.com',
5 onComplete: ({ sessionId, status }) => console.log('done', sessionId, status),
6}).open();

Both produce the same iframe URL — pick whichever fits your codebase.

4. Get the result

Results contain PII and must be retrieved server-side:

$curl https://api.withverifa.com/api/v1/sessions/session_abc123/result \
> -H "X-API-Key: vk_live_your_secret_key"

Or receive results via webhook (recommended for production).


API reference

Verifa.Client class API

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

1const client = new Verifa.Client({
2 captureUrl: session.capture_url, // from POST /api/v1/sessions
3 sessionId: session.id,
4 mode: 'modal', // 'modal' (default) | 'popup' | 'redirect'
5
6 onReady: ({ sessionId }) => console.log('ready', sessionId),
7 onComplete: ({ sessionId, status }) => console.log('done', sessionId, status),
8 onCancel: ({ sessionId }) => console.log('user closed'),
9 onError: ({ code, message }) => console.error(code, message),
10 onEvent: (name, meta) => console.debug('event', name, meta),
11});
12client.open();

Constructor options

OptionTypeRequiredDescription
captureUrlstringOne of these threeCapture URL — either the long-lived session.capture_url or the short-lived embed_url returned by POST /sessions/:id/client-token. The embed URL is the production-grade path.
sessionTokenstringOne of these threeShort-lived JWT from POST /sessions/:id/client-token. Persona-shaped alternative to captureUrl. Must be paired with sessionId.
sessionIdstringRequired with sessionTokenSession ID echoed in callbacks; required when using sessionToken.
publishableKeystringOne of these threevk_pub_* key for quickstart mode (no backend session-create call). The publishable key’s allowed-origins list must include this page’s origin.
templateIdstringNoWorkflow / template ID (quickstart mode).
referenceIdstringNoCustomer-controlled reference ID.
environmentIdstringNoExplicit environment routing.
countrystringNoISO 3166-1 alpha-2 country pre-set.
modestringNo"modal" (default), "popup", or "redirect".
redirectUrlstringNoPost-completion redirect URL (redirect mode only).
metadataobjectNoArbitrary metadata passed to session creation.
apiBasestringNoOverride the API base URL. Defaults to https://app.withverifa.com.
readyTimeoutMsnumberNoTime to wait for onReady before emitting onError({code:"ready_timeout"}). Default 8000. Set to 0 to disable.
onReadyfunctionNoFired when capture page has finished loading.
onCompletefunctionNoFired on successful verification submission.
onCancelfunctionNoFired when the user closes without completing.
onErrorfunctionNoFired on any unrecoverable error.
onEventfunctionNoGeneric event firehose: (name, meta) => void.

Methods

MethodDescription
.open()Mounts the capture UI. Idempotent — calling twice is a no-op.
.cancel()Closes the capture UI and fires onCancel.
.destroy()Closes and tears down without firing any lifecycle events.

Verifa.open(options) — flat / compat form

Equivalent to new Verifa.Client(...).open() but returns a Promise. Useful for await style or existing integrations.

ParameterTypeRequiredDescription
captureUrlstringYesThe capture_url from your session creation response.
sessionIdstringNoThe session ID (returned in callbacks). Recommended.
modestringNo"popup" (default), "modal", or "redirect".
redirectUrlstringNoURL to redirect to after completion (redirect mode only).
onCompletefunctionNoCalled with { sessionId, status } on success.
onErrorfunctionNoCalled with an Error on failure.
onClosefunctionNoCalled when the user closes without completing. Maps to onCancel on the class API.

Returns: Promise<{ sessionId: string, status: string }>

1const result = await Verifa.open({
2 captureUrl: session.capture_url,
3 sessionId: session.id,
4 mode: 'modal',
5});

Display modes

Popup (default)

Opens the capture UI in a centered popup window. Best for desktop.

1Verifa.open({
2 captureUrl: session.capture_url,
3 sessionId: session.id,
4 mode: 'popup',
5 onComplete: function (data) {
6 // Popup closes automatically
7 console.log('Done:', data.sessionId);
8 },
9 onClose: function () {
10 console.log('User closed the popup');
11 },
12});

If the browser blocks the popup, onError fires. Fall back to modal mode.

Modal

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.

1Verifa.open({
2 captureUrl: session.capture_url,
3 sessionId: session.id,
4 mode: 'modal',
5 onComplete: function (data) {
6 console.log('Done:', data.sessionId);
7 },
8});

Redirect

Navigates the browser to the capture page. After completion, the user returns to the redirect_url you specified when creating the session.

1Verifa.open({
2 captureUrl: session.capture_url,
3 mode: 'redirect',
4});

The user lands back on your redirect URL with query parameters:

https://your-app.com/verify/complete?session_id=session_abc123&status=completed

Framework examples

React

1import { useCallback } from 'react';
2import Verifa from '@verifa/sdk'; // or use the global window.Verifa from the CDN script tag
3
4function VerifyButton({ userId }) {
5 const handleClick = useCallback(async () => {
6 // Your backend creates the session
7 const res = await fetch('/api/create-verification', {
8 method: 'POST',
9 headers: { 'Content-Type': 'application/json' },
10 body: JSON.stringify({ userId }),
11 });
12 const session = await res.json();
13
14 new Verifa.Client({
15 captureUrl: session.capture_url,
16 onComplete: ({ sessionId, status }) => {
17 if (status === 'completed') {
18 // Notify your backend; results live server-side
19 fetch('/api/verification-submitted', {
20 method: 'POST',
21 headers: { 'Content-Type': 'application/json' },
22 body: JSON.stringify({ sessionId }),
23 });
24 }
25 },
26 onCancel: () => console.log('User closed'),
27 onError: ({ code, message }) => console.error(code, message),
28 }).open();
29 }, [userId]);
30
31 return <button onClick={handleClick}>Verify Identity</button>;
32}

Next.js (app router)

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.

1'use client';
2import Script from 'next/script';
3import { useCallback } from 'react';
4
5export default function VerifyPage() {
6 const startVerification = useCallback(async () => {
7 const session = await fetch('/api/create-verification', { method: 'POST' })
8 .then(r => r.json());
9
10 new window.Verifa.Client({
11 captureUrl: session.capture_url,
12 onComplete: ({ sessionId }) => router.push(`/verify/complete?id=${sessionId}`),
13 }).open();
14 }, []);
15
16 return (
17 <>
18 <Script
19 src="https://cdn.withverifa.com/dist/verifa-v1.0.0.js"
20 integrity="sha384-/2DZ+OBwe8NRgYq4X0/qpvx7e7s9CjFy0K7WZX4E5utplz4njxn3A0tNTXNsK4fm"
21 crossOrigin="anonymous"
22 strategy="afterInteractive"
23 />
24 <button onClick={startVerification}>Verify Identity</button>
25 </>
26 );
27}

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.

Vue

1<template>
2 <button @click="startVerification">Verify Identity</button>
3</template>
4
5<script setup>
6const startVerification = async () => {
7 const res = await fetch('/api/create-verification', { method: 'POST' });
8 const session = await res.json();
9
10 const result = await window.Verifa.open({
11 captureUrl: session.capture_url,
12 sessionId: session.id,
13 mode: 'modal',
14 });
15 console.log('Done:', result.sessionId);
16};
17</script>

Vanilla JavaScript

1document.getElementById('verify-btn').addEventListener('click', async () => {
2 const res = await fetch('/api/create-verification', { method: 'POST' });
3 const session = await res.json();
4
5 try {
6 const { sessionId, status } = await Verifa.open({
7 captureUrl: session.capture_url,
8 sessionId: session.id,
9 });
10
11 if (status === 'completed') {
12 showSuccess('Verification submitted!');
13 }
14 } catch (err) {
15 showError(err.message);
16 }
17});

Error handling

onError is called with { code, message } for every recoverable and unrecoverable failure. Branch on code for stable handling — message may change between versions.

Error code reference

codeTriggerMessage hintWhat to do
session_create_failedThe POST /api/v1/sessions call failed (publishable-key / quickstart mode only).Echo of the API error: HTTP 401, Origin not allowed, plan-limit text, etc.Inspect message for the underlying API response. Most common cause: the publishable key’s allowed-origins list doesn’t include the host page’s origin.
popup_blockedThe browser blocked window.open in popup mode.Popup blocked. Please allow popups for this site.Fall back to mode: 'modal' and re-call .open(), or instruct the user to allow popups.
ready_timeoutThe capture page didn’t emit verifa:ready within readyTimeoutMs (default 8000 ms).Mentions CSP / Permissions-Policy.The host page’s CSP or Permissions-Policy is almost certainly blocking the iframe or its camera access. See Host page requirements. The iframe stays mounted so you can inspect via dev tools; call .cancel() to clean up.
verification_failedThe capture page itself reported an error (the user couldn’t complete the flow, e.g. document upload repeatedly failed, OCR couldn’t parse, liveness failed).Echo of the capture-side error.This is a real verification failure, not an integration bug. The session moves to a failed status and you should surface that to the user.

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:

1new Verifa.Client({
2 captureUrl: session.capture_url,
3 onComplete: ({ sessionId, status }) => {
4 if (status === 'completed') showSuccess(sessionId);
5 else showRetry(`Verification ${status}`);
6 },
7 onError: ({ code, message }) => {
8 switch (code) {
9 case 'popup_blocked':
10 return openInModalMode();
11 case 'ready_timeout':
12 return showSupportPrompt('The verification window did not load.');
13 case 'session_create_failed':
14 return showSupportPrompt(message); // surface for support
15 case 'verification_failed':
16 return showRetry(message);
17 default:
18 return showSupportPrompt(message);
19 }
20 },
21}).open();

Sandbox testing

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

1// Backend creates session with sandbox key (vk_sandbox_*)
2// Frontend uses the capture_url — same code, no changes needed
3Verifa.open({
4 captureUrl: sandboxSession.capture_url,
5 sessionId: sandboxSession.id,
6 onComplete: function (data) {
7 console.log('Sandbox result:', data.status);
8 },
9});

Edge cases

Mobile Safari before iOS 16.4

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:

1new Verifa.Client({
2 captureUrl: session.capture_url,
3 mode: 'modal',
4 onError: ({ code }) => {
5 if (code === 'ready_timeout') {
6 // Re-open in a new tab — Safari is happy with camera in a top-level page.
7 window.open(session.capture_url, '_blank');
8 }
9 },
10}).open();

Popup blockers

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:

1// PATTERN 1 — open the modal right away (synchronous), no popup blocker risk
2button.onclick = () => new Verifa.Client({ captureUrl, mode: 'modal' }).open();
3
4// PATTERN 2 — fetch first, then open popup. Will be blocked unless the user
5// has allowed popups on your site. Fall back if blocked:
6button.onclick = async () => {
7 const session = await fetch('/api/create-verification', { method: 'POST' })
8 .then(r => r.json());
9 new Verifa.Client({
10 captureUrl: session.capture_url,
11 mode: 'popup',
12 onError: ({ code }) => {
13 if (code === 'popup_blocked') {
14 new Verifa.Client({ captureUrl: session.capture_url, mode: 'modal' }).open();
15 }
16 },
17 }).open();
18};

ready_timeout debugging

If 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:

Console messageHeader to fix
"Content Security Policy directive: 'frame-src ...'"Add frame-src https://app.withverifa.com to your page’s CSP.
"Permissions-Policy ... camera"Add camera=(self "https://app.withverifa.com") to your page’s Permissions-Policy.
"Refused to display in a frame"Check that you aren’t on an old cdn.withverifa.com/dist/verifa-v0.x.js — v1.0.0 dropped the SAMEORIGIN restriction.

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.

Hidden iframe / tab visibility

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.

Strict CSP (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.

Browser support

The SDK uses fetch, Promise, and postMessage, supported in all modern browsers:

  • Chrome 42+, Firefox 39+, Safari 10.1+, Edge 14+

No polyfills required for any browser released after 2017.

Security

  • No API key in the browser — With the recommended 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).
  • Origin allowlist on publishable keys — The optional quickstart mode uses a publishable key (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.
  • Subresource Integrity — The CDN script tag carries an integrity= hash so a tampered script can’t execute. The browser refuses the script if the hash doesn’t match the bytes received.
  • No PII exposure — The SDK never receives verification results, document images, or personal data. Results are fetched server-side or pushed via webhook.
  • Token-based capture — Document uploads inside the capture iframe use a session-specific token, never an API key.

Related

  • Web Capture Flow — Full server-side integration guide
  • Choosing an Integration Method — Compare all integration options
  • Authentication — API key types and scopes
  • Webhooks Guide — Receive results in real-time
  • Testing — Sandbox testing guide