Age Verification
Add compliant, privacy-first age verification to your platform in minutes. ProntoID handles ID scanning, biometric liveness, and regulatory compliance — your platform receives only a secure token confirming 18+.
Introduction
The ProntoID Age Verification API lets your platform confirm that a user meets your minimum age requirement without ever receiving or storing their personal data. When your backend calls verification-start, ProntoID returns a short-lived verification token and URL. You redirect the user there; ProntoID runs document scanning and biometric checks; then POSTs the result to your webhook endpoint. Your platform only ever sees a pass/fail token — never the user's ID, date of birth, or photo.
Each service you create gets its own isolated platform_id and api_key, scoped to a single platform and minimum age threshold. Run separate services per product, region, or compliance framework with full isolation.
How it works
platform_id, client_secret, and static api_key.state value (e.g. bin2hex(random_bytes(16))) and store it in a short-lived secure cookie. This lets you verify the callback has not been forged.client_reference and the state token to the API gateway using your x-api-key and x-platform-id headers. You receive back a verification_token and a ready-to-use redirect URL.verification_token against the user in your database, then redirect them to https://verify.prontoid.com/api/open_platform_session.php?verification_token=…. ProntoID guides them through ID capture and liveness.webhook_url. Match the client_reference to your user, check status, and update your database. Never trust the return URL alone.authorized_user_redirect_uri (verified) or verification_incomplete_url (failed/cancelled). Show a confirmation screen — but always gate access based on the webhook, not the URL.Double-blind privacy
ProntoID's age verification is built on a double-blind architecture, mandated by ARCOM (France) and AGCOM (Italy) and recommended as best practice globally. The two principals in the verification chain — your platform and ProntoID — each know only what they need to:
| Party | Knows | Never knows |
|---|---|---|
| Your platform | User's internal ID (client_reference) · Pass/fail result |
User's real name, date of birth, ID number, or photo |
| ProntoID | User's identity documents (for verification only, not retained) | Which website or platform the user is accessing |
Create a service
Each integration starts with an age verification service. Go to Configure Age Verification, fill in your platform details, and submit. The page will display your platform_id, client_secret, and static api_key — copy the secret immediately, it is shown only once.
A service stores: your platform name (shown to users during verification), your minimum age threshold, compliance flags (ARCOM/AGCOM, GDPR, 2257, COPPA), and six redirect/callback URLs — main URL, webhook, success redirect, return/cancel URL, incomplete URL, and payment-required URL.
API credentials
After creating a service you receive three credentials:
| Credential | Used in | Notes |
|---|---|---|
platform_id |
x-platform-id header |
Public identifier for your platform. Safe to log. |
api_key |
x-api-key header |
Static key. Treat as a secret — never expose client-side. |
client_secret |
Request signing (future) | Shown once at creation. Store in a secrets manager immediately. |
Start a verification
When a user needs to verify their age, your backend POSTs to the verification-start endpoint with their internal user ID and a CSRF state token. You receive a verification_token — save it, then build the redirect URL.
https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/verification-start
// 1. Generate and store a CSRF state token $state = bin2hex(random_bytes(16)); // 32 hex characters setcookie('age_verification_state', $state, [ 'expires' => time() + 900, // 15 minutes 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ]); // 2. Call verification-start $curl = curl_init('https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/verification-start'); curl_setopt($curl, CURLOPT_POST, true); curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode([ 'client_reference' => $userId, 'state' => $state, ])); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'x-api-key: ' . $_ENV['PRONTOID_API_KEY'], 'x-platform-id: ' . $_ENV['PRONTOID_PLATFORM_ID'], ]); try { $response = curl_exec($curl); if (curl_errno($curl)) { throw new Exception(curl_error($curl)); } $http_status = curl_getinfo($curl, CURLINFO_HTTP_CODE); $response_data = json_decode($response, true); if ($http_status >= 200 && $http_status < 300 && isset($response_data['verification_token'])) { $verificationToken = $response_data['verification_token']; // 3. Save the token against the user before redirecting saveAgeVerificationToken($userId, $verificationToken, 'prontoid'); // 4. Build the redirect URL $redirectUrl = 'https://verify.prontoid.com/api/open_platform_session.php' . '?verification_token=' . urlencode($verificationToken); } else { $errorMessage = $response_data['message'] ?? 'Unknown error occurred.'; throw new Exception('API Gateway Error: ' . $errorMessage); } } catch (Exception $e) { $hasError = true; $errorDetails = $e->getMessage(); } finally { curl_close($curl); }
curl -X POST \ https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/verification-start \ -H 'Content-Type: application/json' \ -H 'x-api-key: YOUR_API_KEY' \ -H 'x-platform-id: YOUR_PLATFORM_ID' \ -d '{ "client_reference": "user_abc123", "state": "a3f8c2d1e4b7" }'
Success response 200
{
"success": true,
"verification_token": "vt_…",
"expires_at": 1735430400,
"client_reference": "user_abc123"
}
State parameter
The state parameter serves as a CSRF token for the verification flow. Generate it with a cryptographically secure random function, store it in an httponly cookie, and verify it matches when the user returns to your platform.
state on return. If the value in the callback does not match your cookie, reject the session — it may indicate a CSRF or session fixation attack.// On your authorized_user_redirect_uri page: $returned_state = $_GET['state'] ?? null; $stored_state = $_COOKIE['age_verification_state'] ?? null; if (!$returned_state || !hash_equals($stored_state, $returned_state)) { // State mismatch — abort http_response_code(403); exit('Invalid state.'); } // Clear the state cookie setcookie('age_verification_state', '', time() - 3600, '/'); // Do NOT grant access here — wait for the webhook
Redirect the user
Send the user to the verification URL constructed from the verification_token. The URL is single-use and expires — never cache it or reuse it across sessions.
$redirectUrl = 'https://verify.prontoid.com/api/open_platform_session.php?verification_token=' . urlencode($verificationToken); header('Location: ' . $redirectUrl); exit;
authorized_user_redirect_uri only signals the flow ended — it carries no verification result. Always wait for the webhook before granting access or marking a user as age-verified.Webhook
ProntoID POSTs verification results to the webhook_url you set when creating the service. Your endpoint must respond with HTTP 200; any other status or a timeout triggers a retry with exponential back-off.
Example webhook handler — PHP
// age-verification-webhook.php $payload = json_decode(file_get_contents('php://input'), true); if ($payload['status'] === 'approved') { $userId = $payload['client_reference']; // your internal user ID // Grant access in your database markUserAgeVerified($userId); } elseif ($payload['status'] === 'rejected') { $userId = $payload['client_reference']; // User failed the age check — restrict or notify restrictUserAccess($userId); } // Always respond 200 to acknowledge receipt http_response_code(200); echo json_encode(['received' => true]);
Real-world example
The following is the complete production integration used by a content creator platform. The implementation is split across two files: the server-side PHP that calls the API and builds the redirect URL, and the HTML redirect page shown to the user while the handoff happens.
Part 1 — Server-side: API call & token storage
// --- Authenticate the user --- $accessToken = $YourApp->checkInput($_COOKIE['accessToken']); if (empty($accessToken)) { header('Location: /signin.php?error=session_expired'); exit; } $userId = $YourApp->getUserIdByAccessToken($accessToken); if (empty($userId)) { header('Location: /signin.php?error=user_not_found'); exit; } // --- Generate and store the CSRF state token --- $state = bin2hex(random_bytes(16)); setcookie('age_verification_state', $state, [ 'expires' => time() + 900, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ]); // --- Call the ProntoID API gateway --- $API_ENDPOINT = 'https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/verification-start'; $API_KEY = '••••••••••••••••••••••••••'; // load from env / secrets manager $PLATFORM_ID = 'yourplatform_xxxxxxxxxxxx'; $curl = curl_init($API_ENDPOINT); curl_setopt($curl, CURLOPT_POST, true); curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode([ 'client_reference' => $userId, 'state' => $state, ])); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'x-api-key: ' . $API_KEY, 'x-platform-id: ' . $PLATFORM_ID, ]); try { $response = curl_exec($curl); if (curl_errno($curl)) { throw new Exception(curl_error($curl)); } $http_status = curl_getinfo($curl, CURLINFO_HTTP_CODE); $response_data = json_decode($response, true); if ($http_status >= 200 && $http_status < 300 && isset($response_data['verification_token'])) { $verificationToken = $response_data['verification_token']; $platform = 'yourplatform'; // Persist the token — match it to the user when the webhook arrives $YourApp->saveAgeVerificationToken($userId, $verificationToken, $platform); // Build the ProntoID redirect URL $redirectUrl = 'https://verify.prontoid.com/api/open_platform_session.php' . '?verification_token=' . urlencode($verificationToken); } else { $errorMessage = $response_data['message'] ?? 'Unknown error occurred.'; throw new Exception('API Gateway Error: ' . $errorMessage); } } catch (Exception $e) { $hasError = true; $errorDetails = $e->getMessage(); } finally { curl_close($curl); }
Part 2 — Redirect page: spinner, auto-redirect & error state
<?php if (isset($hasError) && $hasError): ?> <!-- ── Error state ── --> <h1>Verification Error</h1> <p>We encountered an issue while initiating your age verification.</p> <div class="error-container"> <div class="error-heading">Error Details:</div> <div class="error-text"><?= htmlspecialchars($errorDetails) ?></div> </div> <a href="<?= htmlspecialchars($_SERVER['PHP_SELF']) ?>">Try Again</a> <?php else: ?> <!-- ── Success state: spinner + manual fallback ── --> <div class="spinner"></div> <h1>Redirecting to ProntoID...</h1> <p>Please wait while we securely redirect you to complete your age verification.</p> <ul> <li>You'll be taken to ProntoID's secure platform</li> <li>Complete a quick identity verification</li> <li>Return to your platform automatically when done</li> </ul> <p>Not redirecting automatically?</p> <a href="<?= htmlspecialchars($redirectUrl) ?>">Click Here to Continue</a> <?php endif; ?> <!-- Auto-redirect after 2 s, with click-anywhere fallback --> <?php if (!isset($hasError)): ?> <script> setTimeout(function() { window.location.href = <?= json_encode($redirectUrl) ?>; }, 2000); // Clicking anywhere also triggers the redirect immediately document.addEventListener('click', function() { window.location.href = <?= json_encode($redirectUrl) ?>; }); </script> <?php endif; ?>
$API_KEY above is redacted. In production, load it from an environment variable or AWS Secrets Manager — never commit credentials to version control. The platform_id (your platform_id from setup) is safe to store in config as it is not secret.saveAgeVerificationToken() call persists the verification_token against the user in your database before the redirect happens. This lets your webhook handler look up the correct user by token when ProntoID POSTs the result — even if the user closes the tab mid-flow.Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
client_reference |
string | Required | Your internal user ID. Echoed back in the webhook so you can match results to your users. |
state |
string | Required | Cryptographically random CSRF token. Generate with bin2hex(random_bytes(16)). Returned in webhook and redirect callbacks for validation. |
Headers required on every request: x-api-key and x-platform-id.
Response fields
| Field | Type | Description |
|---|---|---|
success | boolean | true when the verification session was created successfully. |
verification_token | string | Short-lived token used to construct the redirect URL. Single-use — save it against the user before redirecting. |
expires_at | integer (Unix) | Unix timestamp after which the token is invalid. Typically 15–30 minutes from creation. |
client_reference | string | Echo of the client_reference you submitted. |
Webhook payload
ProntoID sends a POST with Content-Type: application/json to your webhook URL. Respond with HTTP 200 to acknowledge.
{
"event": "age_verification.completed",
"verification_token": "vt_…",
"client_reference": "user_abc123",
"status": "approved", // "approved" | "rejected" | "pending_review"
"state": "a3f8c2d1e4b7",
"age_confirmed": true, // true = met minimum age threshold
"completed_at": 1735430400,
"checks": {
"id_document": "passed",
"liveness": "passed",
"face_match": "passed"
}
}
Service tiers
| Feature | Basic | Premium | Enterprise |
|---|---|---|---|
| ID document scan | ✓ | ✓ | ✓ |
| Biometric liveness check | ✓ | ✓ | ✓ |
| Face matching | ✓ | ✓ | ✓ |
| Selfie-only estimation | ✓ | ✓ | ✓ |
| Document authentication | — | — | Enterprise |
| Pricing | $0.30 / verification | $0.60 / verification + $29/mo | Custom — contact sales |
Error codes
| HTTP | Message | Cause & fix |
|---|---|---|
400 |
Missing required field | A required parameter was omitted. Check client_reference, state, and both headers. |
401 |
Authentication required | Missing or invalid x-api-key or x-platform-id headers. |
403 |
Permission denied | The API key does not belong to this platform, or the service is inactive. |
409 |
Token conflict | A verification session already exists for this user. Let it expire or complete before starting a new one. |
429 |
Rate limit exceeded | Too many requests. Back off and retry with exponential delay. |
500 |
Server error | Transient server-side error. Retry once; if it persists contact support with the verification_token if available. |