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.
- Need server-side validation? See Server Integration and Sessions API
- Understanding L1 vs L2? See Verification Modes
- Setting up webhooks? See Webhooks
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
| Property | Value |
|---|---|
| npm package | @safepassage/sdk |
| Current version | 3.4.12 |
| Bundle size | ~18KB minified |
| TypeScript | Full type definitions included |
| License | MIT |
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
| Browser | Minimum Version |
|---|---|
| Chrome | 60+ |
| Firefox | 55+ |
| Safari | 12+ |
| Edge | 79+ (Chromium) |
| iOS Safari | 12+ |
| Android Chrome | 60+ |
Requirements:
- Modern JavaScript (ES2015+)
window.crypto.subtlefor HMAC signingURLandURLSearchParamsAPIs
Not Supported:
- Internet Explorer (any version)
- Node.js (browser-only SDK)
- Web Workers (requires
windowobject)
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>
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
localhostand127.0.0.1are 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,sessionIdetc. 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>
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:
| Option | Type | Description |
|---|---|---|
apiKey | string | Required. Publishable key (pk_...). |
returnUrl | string | Required. URL to return users to after verification completes (with status parameter). |
cancelUrl | string | Optional URL to return users to if they close the verification window in new-tab mode. |
defaultChallengeAge | number | Default 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'. |
onComplete | function | Callback when verification completes (new-tab mode only). |
onCancel | function | Callback when user closes popup (new-tab mode only). Return false to suppress automatic cancelUrl redirect. |
onError | function | Callback 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.
| Status | Description |
|---|---|
verified | User successfully verified their age |
failed | Verification failed (age not met, image quality, etc.) |
cancelled | User 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
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>
To test this example:
- Save as
index.html - Run a local web server:
npx serve .orpython3 -m http.server 8000 - Open
http://localhost:8000(or appropriate port) in your browser - Replace
pk_your_public_keywith 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 });
| Option | Effect |
|---|---|
skipIntro | Skips the intro screen; user goes directly to camera access |
autoReturn | Skips the success screen; redirects to returnUrl immediately after verification |
skipIntro: Your site already explains the verification processautoReturn: 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:
- During redirect to SafePassage: Browser shows standard network error
- During verification: SafePassage UI shows error and allows retry
- 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
| Aspect | Value | Notes |
|---|---|---|
| Session timeout | 10 minutes | From creation to completion |
| Inactivity timeout | None | User can pause and resume within 10 minutes |
| Extension | Not possible | Create 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 Conflicterror - 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
Navigation and Session Recovery
What happens if the user navigates away:
| Scenario | Behavior |
|---|---|
| User closes browser tab | Session continues; user has 10 min to return via same link |
| User clicks browser back button | Returns to your page; session still active in SafePassage |
| User opens verification URL in new tab | Session continues in new tab |
| Network disconnection | Session persists; user can reconnect and continue |
| User waits >10 minutes | Session 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
- Don't store session IDs long-term - They're only valid for 10 minutes
- Validate sessions immediately - When user returns, validate right away
- Handle expiration gracefully - Offer to restart verification if session expired
- 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
- Client-side only – The SDK runs in web browsers and does not execute in Node.js.
- Publishable keys only – Use
pk_...in the browser; never expose your secret key (sk_...) — the SDK blocks secret keys in client code. - HTTPS for production redirects – Use HTTPS for
returnUrlin production. Local HTTP is allowed onlocalhost/127.0.0.1. - Blocked protocols – The SDK blocks
file://,javascript:,data:, and other suspicious URL schemes for security protection. - Session IDs are generated by SafePassage – Do not attempt to create your own IDs on public-key flows.
- Domain restrictions – Configure allowed domains in the Dashboard to prevent misuse.
Related Docs
- Getting Started — SDK quickstart
- Server Integration — API-based integration
- Sessions API — API endpoint reference
- Webhooks — Real-time event notifications
- Verification Modes — L1 vs L2 comparison