vouchID Logo

vouchIDDocs

Documentation is subject to change as we continue to harden the product.

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.

Platform Key (Public): pub_platform_your_key_here
Application API Key (Secret): vouch_live_your_api_key_here

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:

ParameterRequiredDescription
platformKeyYesYour platform key (starts with pub_platform_)
modeYesSet to age for 18+ verification
allowAnonymousNoSet to true to enable "Skip for Now" one-time verification option
allowEmailSessionsOptionalDefaults to true. Set to false to hide the email OTP path.
allowPhoneSessionsOptionalDefaults to false. Set to true to allow phone-number OTP sessions alongside (or instead of) email.
regionOptionalForce 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.
parentOriginYesOrigin of your embedding site (e.g., https://your-site.example) so the widget can postMessage back.
redirectUriNoOnly 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.example

With One-Time Verification Option:

https://widget.vouchid.co?platformKey=pub_platform_your_key&mode=age&allowAnonymous=true&parentOrigin=https%3A%2F%2Fyour-site.example

Phone-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.example

Shows 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

TypeWhen You See ItPayload
VOUCHID_ERRORWidget error (non-OAuth){ error: string }
VOUCHID_RESIZEOptional: iframe sizing support{ height: number }
VOUCHID_CLOSEOptional: user closed flow{}
VOUCHID_READYSDK mode readiness{}
vouchid_readyLegacy 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:

FieldTypeDescription
validbooleanWhether the session token is valid
ageVerifiedbooleanWhether user has completed 18+ verification
verificationMethodstringNested under verificationDetails.verificationMethod (e.g., financial_provider)
scopedVidstringScoped user identifier for your application
applicationIdstringApplication that owns the token
issuedAtstringISO timestamp when the token was issued
expiresAtstringISO timestamp when the token expires
sessionExistsbooleanWhether 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 true

Missing 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

Congratulations!

You've successfully integrated vouchID age verification! Your users can now prove they're 18+ without sharing any personal information.