Skip to main content

JavaScript SDK

The SafePassage JavaScript SDK is the fastest way to embed biometric age verification in any web experience. It runs entirely in the browser, automatically creates sessions with your publishable key, and handles the full user flow end to end.

Related Resources

Installation

npm install @safepassage/sdk

Or load it directly from a CDN:

<script src="https://cdn.jsdelivr.net/npm/@safepassage/sdk@latest/safepassage.min.js"></script>

:::note[CDN filenames]
We publish both `safepassage.min.js` (branded) and `sdk.min.js` (generic alias). They point to the same build; use the branded filename shown here.
:::

Package Details

PropertyValue
npm package@safepassage/sdk
Current version3.4.12
Bundle size~18KB minified
TypeScriptFull type definitions included
LicenseMIT

Exports

The SDK supports multiple import styles:

// Named export (recommended)
import { SafePassage } from '@safepassage/sdk';

// Default export
import SafePassage from '@safepassage/sdk';

// Version constant
import { SafePassage, VERSION } from '@safepassage/sdk';
console.log(VERSION); // "3.4.12"

// TypeScript types (automatically available)
import type { SafePassageConfig, VerificationOptions } from '@safepassage/sdk';

For CDN usage, SafePassage is attached to the global window object:

const sdk = new window.SafePassage({ ... });
console.log(window.SafePassage.VERSION); // "3.4.12"

Browser Compatibility

BrowserMinimum Version
Chrome60+
Firefox55+
Safari12+
Edge79+ (Chromium)
iOS Safari12+
Android Chrome60+

Requirements:

  • Modern JavaScript (ES2015+)
  • window.crypto.subtle for HMAC signing
  • URL and URLSearchParams APIs

Not Supported:

  • Internet Explorer (any version)
  • Node.js (browser-only SDK)
  • Web Workers (requires window object)
Checking SDK Version

You can verify the SDK is loaded correctly by checking the version:

console.log(SafePassage.VERSION); // Should output "3.4.12"

Quick Start

Step 1 — Keys

  • Use a publishable key (pk_...) in browser code. Never use secret keys (sk_...) client-side; the SDK blocks them and they expose your account.
  • Get your publishable key from Dashboard → API Keys and paste it as apiKey.

Step 2 — Initialize with a safe return URL

Use the URL API instead of string concatenation to avoid malformed URLs when the current page already has query or hash parameters.

ESM (bundlers)

import { SafePassage } from '@safepassage/sdk';

const sdk = new SafePassage({
apiKey: 'pk_your_public_key', // Publishable key only (pk_...)
returnUrl: window.location.origin + window.location.pathname // Returns here with status param
});

// Start verification on a button click
document.getElementById('verify-age').onclick = () =>
sdk.verify({ externalUserId: currentUser.id });

CDN (no bundler)

<script src="https://cdn.jsdelivr.net/npm/@safepassage/sdk@latest/safepassage.min.js"></script>
<script>
window.addEventListener('load', () => {
const sdk = new SafePassage({
apiKey: 'pk_your_public_key',
returnUrl: window.location.origin + window.location.pathname
});

document.getElementById('verify-age').onclick = () =>
sdk.verify({ externalUserId: currentUser.id });
});
</script>
HTTPS & Local Development

Production requires HTTPS for returnUrl. Local development supports http://localhost and http://127.0.0.1.

Important: file:// URLs are not supported because the verification redirect cannot return to local files. Use a local web server (npx serve ., python3 -m http.server, or your framework's dev server) when testing locally.

Local Development Details

The SDK has specific rules for local development to make testing easy while maintaining security in production:

Allowed for Development:

// All of these work in development
returnUrl: 'http://localhost:3000/callback' // ✅ Any port allowed
returnUrl: 'http://localhost:8080/verify/complete' // ✅ Any path/route allowed
returnUrl: 'http://127.0.0.1:5173/app/verified' // ✅ 127.0.0.1 treated same as localhost
returnUrl: 'http://localhost/callback' // ✅ Default port 80 also works

Blocked Everywhere:

returnUrl: 'file:///Users/dev/index.html'    // ❌ file:// protocol blocked
returnUrl: 'javascript:void(0)' // ❌ javascript: protocol blocked
returnUrl: 'data:text/html,...' // ❌ data: protocol blocked

Production Requirements:

// Production (non-localhost) MUST use HTTPS
returnUrl: 'https://example.com/callback' // ✅ HTTPS required
returnUrl: 'http://example.com/callback' // ❌ HTTP blocked in production

URL Validation Behavior:

  • Domain validation: Only localhost and 127.0.0.1 are exempt from HTTPS requirement
  • Port validation: None - any port (3000, 5173, 8080, etc.) is allowed
  • Path validation: None - any route path (/callback, /verify/done, etc.) is allowed
  • Query parameters: Preserved - SafePassage appends status, sessionId etc. to existing params
  • Allowed Domains (Dashboard setting): Applied in addition to SDK validation; doesn't affect localhost
  • Cancel URLs: Follow the same validation rules as returnUrl

Step 3 — Add the container element referenced by examples

Add this element so the sample result-handling code can reveal restricted content without null reference errors.

<button id="verify-age">Verify your age</button>
<div id="restricted-content" style="display: none"></div>
Local + Production Ready

Using window.location.origin + window.location.pathname creates clean base URLs without existing query parameters. This works seamlessly in both development (http://localhost) and production (https://) environments.

When the user finishes verification, SafePassage redirects back to your returnUrl. You can either read your own markers (e.g., verified=true) or check the standard status/sessionId parameters.

Configuration Options

Required parameters: apiKey and returnUrl:

OptionTypeDescription
apiKeystringRequired. Publishable key (pk_...).
returnUrlstringRequired. URL to return users to after verification completes (with status parameter).
cancelUrlstringOptional URL to return users to if they close the verification window in new-tab mode.
defaultChallengeAgenumberDefault minimum age (25 or higher). Can be overridden per-verification via verify({ challengeAge }).
defaultVerificationMode'L1' | 'L2'Default verification mode. Can be overridden per-verification.
mode'redirect' | 'new-tab''redirect' (default) redirects in same tab; 'new-tab' opens a popup window unless overridden.
newTabTarget'popup' | 'tab''popup' (default) opens a fixed-size window; 'tab' opens a full browser tab. Applies when mode: 'new-tab'.
onCompletefunctionCallback when verification completes (new-tab mode only).
onCancelfunctionCallback when user closes popup (new-tab mode only). Return false to suppress automatic cancelUrl redirect.
onErrorfunctionCallback for errors.

Per-invocation overrides can also be supplied to verify({ ... }); these merge with constructor defaults.

onComplete result shape

{
sessionId: string;
status: 'verified' | 'failed';
timestamp?: number; // ms since epoch
externalUserId?: string; // if provided at session creation
}

Cancellation behavior (new-tab mode): If the user closes the verification window before completion, the SDK fires onCancel and (when cancelUrl is provided) redirects the opener to cancelUrl with status=cancelled, sessionId, and timestamp. Return false from onCancel to suppress the automatic redirect.

Verification Modes

SafePassage supports L1 (face-based estimation) and L2 (document verification). See the Verification Modes Guide for details on when to use each.

sdk.verify({
verificationMode: 'L2', // Force document verification
externalUserId: currentUser.id
});

Track Your Users

Pass an externalUserId when starting verification to correlate SafePassage sessions with your own user records. This should be a unique identifier from your system (e.g., your user's ID, account number, or session identifier):

// Example: Use your application's user ID
sdk.verify({
externalUserId: currentUser.id, // e.g., "usr_abc123"
verificationMode: 'L2',
});

// Or generate a unique session identifier
sdk.verify({
externalUserId: `${userId}-${Date.now()}`, // e.g., "user123-1703123456789"
});

The externalUserId will be included in redirect query parameters, webhook payloads, and API responses, allowing you to link verification results back to your users. This value is safe to use with a publishable key.

Handle Verification Results

SafePassage redirects to your returnUrl with a status query parameter. If you provide a cancelUrl in new-tab mode, cancellations redirect to cancelUrl with status=cancelled.

StatusDescription
verifiedUser successfully verified their age
failedVerification failed (age not met, image quality, etc.)
cancelledUser closed the verification window before completion (new-tab + cancelUrl)
const params = new URLSearchParams(window.location.search);
const status = params.get('status');
const sessionId = params.get('sessionId');

if (status === 'verified') {
// Reveal your gated UI
document.getElementById('restricted-content').style.display = 'block';
} else if (status === 'failed') {
// Verification failed - show age-restricted message or prompt to try again
} else if (status === 'cancelled') {
// User closed the verification window (new-tab mode)
}

Want higher assurance? Use the Server Integration Guide to validate the session with your secret key or consume webhooks for automated handling.

Complete Example

Runnable Examples

Need a full working sample? Contact support@safepassageapp.com and we can share ready-to-run examples for your stack.

Here's a full working example showing all required parameters and proper async loading for CDN usage:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Age Verification</title>
</head>
<body>
<h1>Age-Restricted Content</h1>

<button id="verify-age">Verify Your Age</button>

<div id="restricted-content" style="display: none;">
<h2>Welcome! You've been verified.</h2>
<p>This is the age-restricted content.</p>
</div>

<!-- Load SafePassage SDK from CDN -->
<script src="https://cdn.jsdelivr.net/npm/@safepassage/sdk@latest/safepassage.min.js"></script>

<script>
// Wait for SDK to load
window.addEventListener('load', () => {
// Initialize SafePassage
const sdk = new SafePassage({
apiKey: 'pk_your_public_key', // Replace with your publishable key
returnUrl: window.location.origin + window.location.pathname
});

// Start verification on button click
document.getElementById('verify-age').onclick = () => {
sdk.verify({
externalUserId: 'user_' + Date.now(), // Use your actual user ID
verificationMode: 'L1' // or 'L2' for document verification
});
};

// Handle redirect status
const params = new URLSearchParams(window.location.search);
const status = params.get('status');

if (status === 'verified') {
document.getElementById('restricted-content').style.display = 'block';
document.getElementById('verify-age').style.display = 'none';
} else if (status === 'failed') {
alert('Verification failed. You must be of legal age to access this content.');
}
});
</script>
</body>
</html>
Running This Example Locally

To test this example:

  1. Save as index.html
  2. Run a local web server: npx serve . or python3 -m http.server 8000
  3. Open http://localhost:8000 (or appropriate port) in your browser
  4. Replace pk_your_public_key with your actual publishable key from the Dashboard

Note: Opening the file directly (file://) will not work—the SDK requires http:// or https:// URLs.

Common Patterns

Regional Requirements (challenge age)

  • Client-only (publishable key) — allowed, but client-controlled:
sdk.verify({
verificationMode: 'L1', // e.g., face estimate first
challengeAge: 25 // useful for regional UX; enforce server-side if needed
});

Client-side values can be modified by end users. Use server-side session creation for enforceable requirements.

  • Server-side session (secret key on server only), then client starts with a server-created session:
Server (pseudocode): create a SafePassage session with challengeAge: 25 using your secret key.
// Client: sessionId returned from your server after creating the session
// Use the appropriate SDK/API per the server-side docs
// e.g., redirect to verifyUrl or start with a provided session depending on integration
sdk.verify({ /* ... */ });

See the Server Integration guide for creating sessions with a secret key: /guides/server-integration

Track User Verification

sdk.verify({
externalUserId: 'user123',
});

Streamlined Flows

For embedded experiences where users have already seen consent language or you want minimal interruption:

// Skip intro screen (go directly to camera)
sdk.verify({ skipIntro: true });

// Auto-redirect after success (no success screen)
sdk.verify({ autoReturn: true });

// Both - minimal user interaction
sdk.verify({ skipIntro: true, autoReturn: true });
OptionEffect
skipIntroSkips the intro screen; user goes directly to camera access
autoReturnSkips the success screen; redirects to returnUrl immediately after verification
When to Use
  • skipIntro: Your site already explains the verification process
  • autoReturn: Embedding in multi-step flows (checkout, sign-up)
  • Both: Maximum streamlining (camera → verify → return)

Avoid if users need to read verification instructions or see confirmation.

Error Handling

The SDK provides clear error messages for common issues. Here's how to handle them:

CDN Loading Failures

If the SDK fails to load from the CDN, your verification button won't work. Handle this gracefully:

// Check if SDK loaded successfully
if (typeof SafePassage === 'undefined') {
console.error('SafePassage SDK failed to load');
document.getElementById('verify-age').disabled = true;
document.getElementById('verify-age').textContent = 'Verification unavailable';
// Optionally: show fallback UI or retry loading
}

For critical applications, consider a fallback CDN or self-hosting:

<!-- Primary CDN -->
<script src="https://cdn.jsdelivr.net/npm/@safepassage/sdk@latest/safepassage.min.js"></script>

<!-- Fallback if primary fails -->
<script>
if (typeof SafePassage === 'undefined') {
document.write('<script src="https://unpkg.com/@safepassage/sdk@latest/safepassage.min.js"><\/script>');
}
</script>

SDK Initialization Errors

Initialization throws synchronous errors for invalid configuration:

try {
const sdk = new SafePassage({
apiKey: 'pk_your_key',
returnUrl: 'https://example.com/callback'
});
} catch (error) {
console.error('SDK initialization failed:', error.message);
// Common errors:
// - "apiKey is required"
// - "returnUrl is required"
// - "Secret keys (sk_) should never be used in browser code..."
// - "HTTPS required for return URLs in production"
// - "Blocked suspicious URL scheme"
}

Verification Flow Errors

Errors during the verification flow are handled via the redirect or callbacks:

// For redirect mode: check URL parameters on return
const params = new URLSearchParams(window.location.search);
const status = params.get('status');
const error = params.get('error');

if (status === 'failed') {
// Handle verification failure
console.log('Verification failed');
}

// For new-tab mode: use callbacks
const sdk = new SafePassage({
apiKey: 'pk_your_key',
returnUrl: 'https://example.com/callback',
cancelUrl: 'https://example.com/cancelled',
mode: 'new-tab',
newTabTarget: 'tab',
onError: (error) => {
console.error('Verification error:', error.message);
},
onComplete: (result) => {
if (result.status === 'verified') {
console.log('User verified:', result.sessionId);
} else {
console.log('Verification failed');
}
},
onCancel: () => {
console.log('User closed verification window');
// Return false if you want to handle navigation manually
// return false;
}
});

Network and Timeout Errors

The SDK handles network issues internally during the redirect flow. If the user's connection fails mid-verification:

  1. During redirect to SafePassage: Browser shows standard network error
  2. During verification: SafePassage UI shows error and allows retry
  3. During redirect back: User may need to retry from your page

For server-side integrations, handle API errors appropriately:

// Server-side: handle session creation errors
try {
const session = await createSession();
} catch (error) {
if (error.status === 401) {
console.error('Invalid API key');
} else if (error.status === 429) {
console.error('Rate limited - try again later');
} else if (error.status >= 500) {
console.error('SafePassage service error - try again later');
}
}

Rate Limiting

The SDK includes client-side rate limiting (5 verification attempts per minute per user). If exceeded:

// The SDK will log a warning:
// "SafePassage Security: Rate limit exceeded for [identifier]"

// Handle by showing a message to users
sdk.verify({ externalUserId: userId }).catch((error) => {
if (error.message.includes('Rate limit')) {
alert('Too many attempts. Please wait a minute and try again.');
}
});

Session Lifecycle

Understanding how verification sessions work helps you build robust integrations.

Session Duration

AspectValueNotes
Session timeout10 minutesFrom creation to completion
Inactivity timeoutNoneUser can pause and resume within 10 minutes
ExtensionNot possibleCreate a new session if expired

Session States

Sessions progress through these states:

PENDING → VERIFIED (success)
→ FAILED (verification failed, user closed window, etc.)
→ EXPIRED (10 minute timeout)

Single-Use Sessions

Sessions are single-use only:

  • Once a session reaches any terminal state (VERIFIED, FAILED, EXPIRED), it cannot be reused
  • Attempting to validate an already-validated session returns a 409 Conflict error
  • Each verification attempt requires a new session
// ❌ Don't reuse sessions
const sessionId = 'already-verified-session';
// This will fail with 409 Conflict

// ✅ Create a new session for each verification
sdk.verify({ externalUserId: userId }); // SDK creates a fresh session

What happens if the user navigates away:

ScenarioBehavior
User closes browser tabSession continues; user has 10 min to return via same link
User clicks browser back buttonReturns to your page; session still active in SafePassage
User opens verification URL in new tabSession continues in new tab
Network disconnectionSession persists; user can reconnect and continue
User waits >10 minutesSession expires; user is redirected with status=failed

Mobile handoff (QR code):

  • Desktop user can scan QR to continue on mobile
  • Desktop polls for completion and redirects when mobile finishes
  • Session remains valid across device switch

Best Practices

  1. Don't store session IDs long-term - They're only valid for 10 minutes
  2. Validate sessions immediately - When user returns, validate right away
  3. Handle expiration gracefully - Offer to restart verification if session expired
  4. Use externalUserId - Track your users across multiple verification attempts
// Good pattern: validate immediately on return
const params = new URLSearchParams(window.location.search);
const sessionId = params.get('sessionId');
const status = params.get('status');

if (sessionId && status === 'verified') {
// Validate server-side immediately
const result = await fetch('/api/validate-verification', {
method: 'POST',
body: JSON.stringify({ sessionId })
});

if (result.accessGranted) {
grantAccess();
}
}

Security Notes

  1. Client-side only – The SDK runs in web browsers and does not execute in Node.js.
  2. Publishable keys only – Use pk_... in the browser; never expose your secret key (sk_...) — the SDK blocks secret keys in client code.
  3. HTTPS for production redirects – Use HTTPS for returnUrl in production. Local HTTP is allowed on localhost/127.0.0.1.
  4. Blocked protocols – The SDK blocks file://, javascript:, data:, and other suspicious URL schemes for security protection.
  5. Session IDs are generated by SafePassage – Do not attempt to create your own IDs on public-key flows.
  6. Domain restrictions – Configure allowed domains in the Dashboard to prevent misuse.