Next.js Example
A Next.js App Router integration with a client component for the embed widget and a server-side API route that proxies status checks with the secret key.
Client Component
Create a 'use client' component that renders the iframe and handles postMessage events. This component uses the publishable key which is safe for browser use.
'use client';
import { useState, useEffect, useCallback } from 'react';
const AGEEVIDENCE_URL = 'https://ageevidence.com';
interface VerificationEmbedProps {
apiKey: string;
externalId: string;
level?: 'age_only' | 'full_age' | 'full_kyc';
theme?: 'light' | 'dark' | 'auto';
locale?: 'en' | 'es' | 'pt' | 'fr';
}
export function VerificationEmbed({
apiKey,
externalId,
level = 'full_age',
theme = 'dark',
locale = 'en',
}: VerificationEmbedProps) {
const [loaded, setLoaded] = useState(false);
const [result, setResult] = useState<{
status: string;
verificationId?: string;
} | undefined>(undefined);
const handleMessage = useCallback((event: MessageEvent) => {
if (event.origin !== AGEEVIDENCE_URL) return;
const { type, data } = event.data;
switch (type) {
case 'ae:ready':
setLoaded(true);
break;
case 'ae:complete':
setResult({ status: 'submitted', verificationId: data.verificationId });
pollStatus(externalId);
break;
case 'ae:error':
setResult({ status: `error: ${data.error}` });
break;
case 'ae:cancel':
setResult({ status: 'cancelled' });
break;
}
}, [externalId]);
useEffect(() => {
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [handleMessage]);
const pollStatus = async (id: string) => {
// Call YOUR API route (see api/verify/status/route.ts below)
const res = await fetch(`/api/verify/status?externalId=${id}`);
const data = await res.json();
if (data.data?.status === 'approved') {
setResult({ status: 'approved' });
} else if (data.data?.status === 'rejected') {
setResult({ status: 'rejected' });
} else {
// Continue polling (see Status Polling docs for backoff)
setTimeout(() => pollStatus(id), 5000);
}
};
const params = new URLSearchParams({
apiKey,
externalId,
theme,
locale,
parentOrigin: typeof window !== 'undefined' ? window.location.origin : '',
});
if (result?.status === 'approved') {
return <div>Verification approved.</div>;
}
return (
<div style={{ position: 'relative' }}>
{!loaded && <div className="absolute inset-0 flex items-center justify-center bg-gray-900 rounded-xl">Loading...</div>}
<iframe
src={`${AGEEVIDENCE_URL}/embed/${level}?${params}`}
width="100%"
height="600px"
frameBorder="0"
allow="camera"
style={{ border: 'none', borderRadius: 12, opacity: loaded ? 1 : 0 }}
/>
</div>
);
}API Route (Server-Side)
Create an API route that proxies status checks to AgeEvidence using the secret key. This key is only available on the server through environment variables.
// app/api/verify/status/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const externalId = request.nextUrl.searchParams.get('externalId');
if (!externalId) {
return NextResponse.json({ error: 'Missing externalId' }, { status: 400 });
}
const response = await fetch(
`https://ageevidence.com/v1/verify/${externalId}/status`,
{
headers: {
'X-API-Key': process.env.AGEEVIDENCE_SECRET_KEY!, // sk_verify_...
},
}
);
if (!response.ok) {
return NextResponse.json(
{ error: 'Failed to fetch status' },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
}Add your secret key to .env.local:
AGEEVIDENCE_SECRET_KEY=sk_verify_YOUR_SECRET_KEYPage Usage
Use the client component in a page. The page itself can be a server component.
// app/verify/page.tsx
import { VerificationEmbed } from '@/components/VerificationEmbed';
export default function VerifyPage() {
return (
<main className="max-w-2xl mx-auto py-12 px-4">
<h1 className="text-2xl font-bold mb-6">Verify Your Identity</h1>
<VerificationEmbed
apiKey="pk_verify_YOUR_KEY"
externalId="user_123"
level="full_age"
theme="dark"
locale="en"
/>
</main>
);
}Notes
- The client component must be marked with
'use client'because it usesuseStateanduseEffect. - The API route runs on the server and safely holds the secret key.
- For production, add proper error handling and polling backoff. See the Status Polling guide.
- The
allow="camera"attribute on the iframe is required.