Menu

Back to Dashboard
Developer Docs

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+.

Overview

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.

Overview

How it works

1
Create an age verification service
On the Configure page, set your platform name, minimum age, compliance frameworks, and all redirect URLs. ProntoID generates your platform_id, client_secret, and static api_key.
2
Generate a CSRF state token
Before calling the API, generate a cryptographically random 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.
3
Your backend calls verification-start
POST your user's internal ID as 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.
4
Save the token and redirect the user
Persist the 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.
5
Receive the result via webhook
Once verification completes, ProntoID POSTs a JSON payload to your webhook_url. Match the client_reference to your user, check status, and update your database. Never trust the return URL alone.
6
Redirect back to your platform
After the flow, the user lands on your 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.
Overview

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:

PartyKnowsNever 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
Biometric data and document images are deleted immediately after the verification check completes. ProntoID retains only a cryptographic result token, never raw identity data.
Setup

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.

Setup

API credentials

After creating a service you receive three credentials:

CredentialUsed inNotes
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.
Client secret shown once. It is only returned at creation time. Store it in an environment variable or secrets manager (e.g. AWS Secrets Manager) immediately. If lost, rotate via your dashboard.
Never expose credentials client-side. All API calls must originate from your server. Never include them in frontend JavaScript, mobile app bundles, or version control.
Integration

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.

POST https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/verification-start
PHP
// 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
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

JSON
{
  "success":            true,
  "verification_token": "vt_…",
  "expires_at":         1735430400,
  "client_reference":   "user_abc123"
}
Integration

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.

Always validate 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.
PHP — state validation on return
// 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
Integration

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.

PHP
$redirectUrl = 'https://verify.prontoid.com/api/open_platform_session.php?verification_token='
             . urlencode($verificationToken);

header('Location: ' . $redirectUrl);
exit;
The 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.
Integration

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.

The webhook is the only authoritative source of truth for age verification status. Never rely on query parameters from the return or redirect URLs.

Example webhook handler — PHP

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]);
Integration

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

PHP — kyc.php (server logic)
// --- 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/HTML — redirect template
<?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; ?>
The $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.
Why save the token before redirecting? The 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.
Reference

Request parameters

ParameterTypeRequiredDescription
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.

Reference

Response fields

FieldTypeDescription
successbooleantrue when the verification session was created successfully.
verification_tokenstringShort-lived token used to construct the redirect URL. Single-use — save it against the user before redirecting.
expires_atinteger (Unix)Unix timestamp after which the token is invalid. Typically 15–30 minutes from creation.
client_referencestringEcho of the client_reference you submitted.
Reference

Webhook payload

ProntoID sends a POST with Content-Type: application/json to your webhook URL. Respond with HTTP 200 to acknowledge.

JSON — approved
{
  "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"
  }
}
Reference

Service tiers

FeatureBasicPremiumEnterprise
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
Reference

Error codes

HTTPMessageCause & 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.