Widget Quickstart
Get up and running with vouchID's age verification widget in under 10 minutes. The widget sends only secure tokens to your frontend - all verification data is retrieved via backend token exchange for maximum security.
What You'll Build
A working age verification flow that proves users are 18+ through secure financial verification, using secure token exchange - no verification data is ever sent to the client.
Overview
The vouchID widget integration involves three main steps:
1. Get Platform Key
Register your application and obtain your platform key
2. Embed iframe
Add iframe and listen for secure token (no verification data)
3. Token Exchange
Exchange secure tokens for verification data on your backend
Step 1: Get Your Platform Key
The vouchID age verification widget only requires a single platform key - no complex OAuth setup needed.
Getting Your Platform Key
Your Platform Key (public) and Application API Key (secret) are managed in the vouchID Console. Create an application to view your Platform Key and generate an API key.
If your account is gated/beta and you don't see keys yet, contact support to be provisioned.
Environment Setup
Store your platform key in your environment variables:
Step 2: Embed the iframe
The vouchID widget is embedded as a simple iframe with your platform key. Users complete verification within the iframe, and only a secure token is sent to your app via postMessage - no verification data is exposed to the client.
Secure Token-Only Integration
Just add an iframe with your platform key. Widget sends only secure tokens to your frontend - all verification data stays on your backend.
Important (required): include parentOrigin (your site origin) in the iframe URL so the widget can postMessage back to the correct parent. Using the SDK will set this automatically.
Basic iframe Embed
Add the widget iframe to your page with your platform key. On page load, you may want to check if the user has an existing token from a previous session and validate it with your backend before showing the widget - only display the widget if the token is invalid or missing.
Local Development
In production, the widget is hosted at https://widget.vouchid.co. For local development, the widget runs at http://localhost:3003.
If you validate postMessage origins (recommended), allowlist the production origin and the local origin in development.
import React, { useEffect, useState } from 'react';
interface MessageEvent {
origin: string;
data: {
type: string;
payload?: { platformSessionToken?: string; code?: string; message?: string };
error?: string;
};
}
interface AgeGateProps {
children: React.ReactNode;
platformKey: string;
}
export function AgeGate({ children, platformKey }: AgeGateProps) {
const [isVerified, setIsVerified] = useState(
() => localStorage.getItem('age_verified') === 'true'
);
useEffect(() => {
const handleMessage = async (event: MessageEvent) => {
// Security: Only accept messages from vouchID widget
const allowedOrigins = [
'https://widget.vouchid.co',
...(process.env.NODE_ENV === 'development' ? ['http://localhost:3003'] : []),
];
if (!allowedOrigins.includes(event.origin)) return;
if (event.data.type === 'VOUCHID_SUCCESS') {
const token = event.data.payload?.platformSessionToken;
console.log('Received secure token (no verification data):', { tokenReceived: !!token });
try {
// REQUIRED: Exchange token for verification data on backend
const response = await fetch('/api/vouchid/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
platformSessionToken: token
})
});
const validation = await response.json();
// Backend returns HTTP 200 even when token is invalid; check both response.ok and validation.valid
if (response.ok && validation.valid && validation.ageVerified) {
localStorage.setItem('age_verified', 'true');
localStorage.setItem('verification_token', token || '');
setIsVerified(true);
} else {
throw new Error(validation.error || 'Backend validation failed');
}
} catch (error) {
console.error('Validation error:', error);
alert('Verification validation failed. Please try again.');
}
} else if (event.data.type === 'VOUCHID_FAILURE' || event.data.type === 'VOUCHID_ERROR') {
const message =
event.data.payload?.message ||
event.data.payload?.code ||
event.data.error ||
'Verification failed';
console.error('Age verification failed:', message);
alert(`Verification failed: ${message}`);
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, []);
if (isVerified) {
return <>{children}</>;
}
return (
<div className="age-gate max-w-md mx-auto p-6 bg-white rounded-lg shadow-lg">
<h2 className="text-2xl font-bold mb-4">Age Verification Required</h2>
<p className="mb-6 text-gray-600">
To access this content, please verify that you are 18 or older using
your payment account (PayPal or Revolut).
</p>
{/* vouchID Age Verification Widget */}
<iframe
src={`https://widget.vouchid.co?platformKey=${platformKey}&mode=age&allowAnonymous=true&parentOrigin=${encodeURIComponent(window.location.origin)}`}
width="400"
height="600"
className="w-full border-0 rounded-lg shadow-md"
title="Age Verification Widget"
/>
</div>
);
}
// Usage Example
export default function App() {
return (
<AgeGate platformKey={process.env.NEXT_PUBLIC_VOUCHID_PLATFORM_KEY!}>
<div className="p-8">
<h1>Welcome! You are verified as 18+</h1>
<p>This is the age-restricted content.</p>
</div>
</AgeGate>
);
}One-Time vs. Persistent Verification
When allowAnonymous=true is set, users see a "Skip for Now" option that enables one-time verification without creating an account.
- With account: Users enter email/phone, verification is saved for future use
- One-time: Users skip account creation, verification expires when session ends
iframe Configuration
Configure the widget by adding URL parameters to the iframe src:
| Parameter | Required | Description |
|---|---|---|
| platformKey | Yes | Your platform key (starts with pub_platform_) |
| mode | Yes | Set to age for 18+ verification |
| allowAnonymous | No | Set to true to enable "Skip for Now" one-time verification option |
| allowEmailSessions | Optional | Defaults to true. Set to false to hide the email OTP path. |
| allowPhoneSessions | Optional | Defaults to false. Set to true to allow phone-number OTP sessions alongside (or instead of) email. |
| region | Optional | Force specific provider availability (e.g., region=EU). The widget automatically detects the most likely region from the end user's IP if this is omitted. |
| parentOrigin | Yes | Origin of your embedding site (e.g., https://your-site.example) so the widget can postMessage back. |
| redirectUri | No | Only required for identity verification mode (used with sdkMode=true). |
Use allowEmailSessions and allowPhoneSessions to control whether the widget offers email OTP, phone OTP, or both. Defaults: email is on and phone is off unless allowAnonymous=true is present without either toggle, in which case both are off (anonymous-only flow).
Example URLs
Basic Age Verification (include your origin):
https://widget.vouchid.co?platformKey=pub_platform_your_key&mode=age&parentOrigin=https%3A%2F%2Fyour-site.exampleWith One-Time Verification Option:
https://widget.vouchid.co?platformKey=pub_platform_your_key&mode=age&allowAnonymous=true&parentOrigin=https%3A%2F%2Fyour-site.examplePhone-only OTP (disable email):
https://widget.vouchid.co?platformKey=pub_platform_your_key&mode=age&allowPhoneSessions=true&allowEmailSessions=false&parentOrigin=https%3A%2F%2Fyour-site.exampleShows only the phone-number OTP path. Combine with allowAnonymous=true if you also want the skip flow.
Regional fallbacks handled for you
Passing a region query param lets you control which providers appear, but it's optional. When you omit it, the widget automatically detects the end user's region using IP-based geolocation and picks the best regional configuration.
postMessage Response Format
The iframe sends ONLY a secure token - no verification data:
Note: In some flows (for example, anonymous-only), platformSessionToken may be null. Treat a null token as “not verified” and fall back to showing the widget again.
On verification failure, you'll receive:
Other Event Types
| Type | When You See It | Payload |
|---|---|---|
| VOUCHID_ERROR | Widget error (non-OAuth) | { error: string } |
| VOUCHID_RESIZE | Optional: iframe sizing support | { height: number } |
| VOUCHID_CLOSE | Optional: user closed flow | {} |
| VOUCHID_READY | SDK mode readiness | {} |
| vouchid_ready | Legacy readiness message | { action: "vouchid_ready" } |
Security: Token-Only Design
The postMessage only contains the secure token. All verification details (age verified status, expiry) are only available through backend validation. This prevents frontend tampering and ensures users must complete genuine verification.
Step 3: Backend Token Exchange (Required)
The widget sends only a secure token to your frontend. You must exchange the platformSessionToken on your backend to get the actual verification data. No verification information is ever sent directly to the client.
Token Exchange Required
The widget never sends verification data to the client. Only secure tokens are sent to the frontend. All verification data must be retrieved via backend token exchange to prevent tampering.
Backend Token Exchange
Create an API endpoint to exchange the platformSessionToken for verification data from vouchID's servers:
Note: /api/verifications/validate-platform-token returns HTTP 200 even when the token is invalid. Always checkvalidationData.valid in the response body before trusting the result.
Persist Verification Server-Side
After the backend validates the platform session token, issue a server-owned session (for example, set an HttpOnly cookie or mark the user as verified in your database). Use that session for gating protected pages so the browser cannot spoof verification by editing localStorage.
Client storage can still hide the iframe for convenience, but the server must make the final decision using the session you control.
// app/api/vouchid/validate/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const { platformSessionToken } = await request.json();
// Exchange the token for verification data with vouchID API
const validationResponse = await fetch('https://api.vouchid.co/api/verifications/validate-platform-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.VOUCHID_API_KEY}` // Your application API key
},
body: JSON.stringify({
platformSessionToken: platformSessionToken
})
});
const verificationData = await validationResponse.json();
// NOTE: The API returns HTTP 200 even when tokens are invalid; check validationData.valid
if (!validationResponse.ok || !verificationData.valid) {
return NextResponse.json(
{ valid: false, error: verificationData.error || 'Invalid token' },
{ status: validationResponse.status || 200 }
);
}
// Store user's verified status in your database or session
// Example: Update user record with age_verified: true
return NextResponse.json({
valid: true,
ageVerified: verificationData.ageVerified,
scopedVid: verificationData.scopedVid,
applicationId: verificationData.applicationId,
issuedAt: verificationData.issuedAt,
expiresAt: verificationData.expiresAt,
verificationDetails: verificationData.verificationDetails
});
} catch (error) {
console.error('Token exchange error:', error);
return NextResponse.json({ error: 'Token exchange failed' }, { status: 500 });
}
}Validation Response
If you validate tokens on your backend, the validation response includes:
| Field | Type | Description |
|---|---|---|
| valid | boolean | Whether the session token is valid |
| ageVerified | boolean | Whether user has completed 18+ verification |
| verificationMethod | string | Nested under verificationDetails.verificationMethod (e.g., financial_provider) |
| scopedVid | string | Scoped user identifier for your application |
| applicationId | string | Application that owns the token |
| issuedAt | string | ISO timestamp when the token was issued |
| expiresAt | string | ISO timestamp when the token expires |
| sessionExists | boolean | Whether the session still exists in vouchID (false means re-verification required) |
Complete Integration Examples
Here are complete working examples for different frameworks:
// components/AgeVerificationGate.tsx
import React, { useEffect, useState } from 'react';
type WidgetMessage =
| {
type: 'VOUCHID_SUCCESS';
payload?: { platformSessionToken?: string };
}
| {
type: 'VOUCHID_FAILURE' | 'VOUCHID_ERROR';
payload?: { code?: string; message?: string };
};
export function AgeVerificationGate({
children,
platformKey,
}: {
children: React.ReactNode;
platformKey: string;
}) {
const [isVerified, setIsVerified] = useState(
() => localStorage.getItem('age_verified') === 'true'
);
const [status, setStatus] = useState<'idle' | 'validating' | 'error'>('idle');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (isVerified) {
return;
}
const handleMessage = async (event: MessageEvent<WidgetMessage>) => {
const allowedOrigins = [
'https://widget.vouchid.co',
...(process.env.NODE_ENV === 'development' ? ['http://localhost:3003'] : []),
];
if (!allowedOrigins.includes(event.origin)) return;
if (event.data.type === 'VOUCHID_SUCCESS') {
const token = event.data.payload?.platformSessionToken;
if (!token) {
setStatus('error');
setError('Verification token missing.');
return;
}
setStatus('validating');
setError(null);
try {
const response = await fetch('/api/vouchid/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ platformSessionToken: token }),
});
const result = await response.json();
if (response.ok && result.valid && result.ageVerified) {
localStorage.setItem('age_verified', 'true');
setIsVerified(true);
} else {
throw new Error(result.error || 'Token exchange failed');
}
} catch (err) {
console.error('Token exchange failed', err);
setStatus('error');
setError('Verification failed. Please try again.');
} finally {
setStatus('idle');
}
}
if (event.data.type === 'VOUCHID_FAILURE' || event.data.type === 'VOUCHID_ERROR') {
setStatus('error');
setError(
event.data.payload?.message ||
event.data.payload?.code ||
'Verification was cancelled.'
);
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [isVerified]);
if (isVerified) {
return <>{children}</>;
}
return (
<div className="age-gate">
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-lg">
<h2 className="text-2xl font-bold mb-4">Age Verification Required</h2>
<p className="mb-6 text-gray-600">
Complete the verification to access restricted content.
</p>
<iframe
src={`https://widget.vouchid.co?platformKey=${platformKey}&mode=age&allowAnonymous=true&parentOrigin=${encodeURIComponent(window.location.origin)}`}
width="100%"
height="600"
className="border-0 rounded-lg shadow-md"
title="vouchID age verification"
allow="camera *; microphone *"
/>
{status === 'validating' && (
<p className="mt-4 text-gray-500">Validating verification...</p>
)}
{status === 'error' && error && (
<p className="mt-4 text-red-600">{error}</p>
)}
</div>
</div>
);
}Security Considerations
Frontend-Only Implementation Vulnerabilities
The following patterns in your codebase enable users to bypass age verification entirely through browser manipulation:
Trusting localStorage
Code that checks localStorage.getItem('age_verified') without backend validation:
// VULNERABLE: Users can set this manually
if (localStorage.getItem('age_verified') === 'true') {
showAgeRestrictedContent();
}Frontend-Only Token Storage
Storing and trusting verification tokens in localStorage without backend validation:
// VULNERABLE: Users can fake these tokens
localStorage.setItem('verification_token', 'fake_token');
localStorage.setItem('verification_status', 'verified');Client-Side Only State Management
React/Vue components that manage verification state without server validation:
// VULNERABLE: Users can manipulate component state
const [isVerified, setIsVerified] = useState(true); // manually set to trueMissing Server-Side Checks
API endpoints that don't verify user age verification status:
// VULNERABLE: Missing age verification check
app.get('/age-restricted-content', (req, res) => {
// Should check user's verified status in database
res.json({ restrictedData: 'sensitive content' });
});Secure Implementation Pattern
Always validate verification status on your backend and store results in your secure database:
Security Architecture Summary
Token-Only Client Communication
- Widget sends ZERO verification data to client JavaScript
- Only secure, signed, expiring tokens are sent via postMessage
- All sensitive verification data (age status, PII, etc.) stays on backend
- Prevents client-side tampering, XSS attacks, and data exposure
- Tokens are signed and short-lived (2h TTL); always validate them server-side before trusting
Testing Your Integration
Test Checklist
- Widget loads correctly and shows verification options
- Widget sends ONLY secure tokens (no verification data to client)
- Backend token exchange endpoint correctly retrieves verification data
- User verification status is stored in your database
- Age-restricted content is properly protected server-side
- Security test: Clear localStorage and ensure verification is still required
- Security test: Manually set localStorage values and ensure they're ignored
Next Steps
Set Up Webhooks
Get real-time notifications when verifications complete
API Reference
Explore all available endpoints and options
Congratulations!
You've successfully integrated vouchID age verification! Your users can now prove they're 18+ without sharing any personal information.