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 create-age-verification, 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 creates a verification
POST your user's internal ID as client_reference and the state value, authenticating with your X-API-Key and X-API-Secret headers. You receive back a verification_token and a ready-to-use verification_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

Creating a service returns three values. Two of them authenticate every API call and must be sent together:

CredentialUsed inNotes
api_key X-API-Key header Identifies which platform is calling. Treat as a secret. Rotatable from your dashboard.
client_secret X-API-Secret header Shown once at creation. Proves the call is really from you. Cannot be reissued — contact support if lost.
platform_id Reference only Public identifier, safe to log. Not sent as a header — ProntoID resolves it from your API key.
Why two credentials. The key says who you are; the secret proves it. A leaked key on its own cannot create verifications, and the platform a request applies to is taken from the key rather than from a header — so no caller can act on a platform it does not own.
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 create-age-verification with your identifier for that user and a CSRF state value. You receive a verification_url — store the verification_token against your user, then send them to that URL.

POST https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/create-age-verification
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. Create the verification
$curl = curl_init('https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/create-age-verification');
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-API-Secret: ' . $_ENV['PRONTOID_API_SECRET'],
]);

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/create-age-verification \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -H 'X-API-Secret: YOUR_CLIENT_SECRET' \
  -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 the verification result to the webhook_url you set when creating the service. Two events are sent: age_verification.succeeded and age_verification.failed. Respond with HTTP 200 once you have stored the result.

The webhook is the only authoritative source of verification status. Never rely on query parameters from the return or redirect URLs — those are visible to the user and can be edited.
Verify the signature before you trust anything in the body. Your webhook URL is reachable by anyone. Without this check, a single unauthenticated POST can mark any account age-verified — which defeats the control entirely. Verification is a few lines; the examples below include it.

Signature

Every delivery carries an X-Pronto-Signature header: the HMAC-SHA256 of the exact raw request body, hex-encoded, keyed with the webhook secret shown once when your service was created. Compute it over the raw bytes — decoding and re-encoding the JSON changes key order and whitespace, and the signature will never match. Compare using a constant-time function (hash_equals, hmac.compare_digest), not ===.

No automatic retry. A delivery that fails or times out is not resent. Do not return a non-200 expecting redelivery — store the result first, then acknowledge. If your database write fails, record the payload somewhere durable rather than relying on ProntoID to try again.

Example webhook handler — PHP

PHP
// age-verification-webhook.php

// 1. Read the RAW body — the signature is over these exact bytes.
$raw = file_get_contents('php://input');

// 2. Verify the signature BEFORE parsing or trusting anything.
$signature = $_SERVER['HTTP_X_PRONTO_SIGNATURE'] ?? '';
$expected  = hash_hmac('sha256', $raw, $_ENV['PRONTOID_WEBHOOK_SECRET']);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$payload = json_decode($raw, true);
$event   = $payload['type'];
$data    = $payload['data']['object'];  // fields are nested here

// 3. Resolve the user from YOUR OWN record of the token you stored
//    when you started the verification. client_reference is a hint —
//    never the source of truth for which account to update.
$row = findVerificationByToken($data['verification_token']);
if (!$row) {
    http_response_code(404);
    exit;
}

// 4. Approve only if the checks passed AND the age threshold was met.
//    is_authentic alone is not enough — a verified minor is authentic.
$approved = $event === 'age_verification.succeeded'
    && $data['is_authentic']
    && $data['is_adult'];

setUserAgeVerified($row['user_id'], $approved);

http_response_code(200);
echo json_encode(['received' => true]);

Example webhook handler — Python

Python
import hmac, hashlib, json, os

def handle(raw_body: bytes, headers: dict):
    # 1. Verify over the RAW bytes, before parsing.
    expected = hmac.new(
        os.environ['PRONTOID_WEBHOOK_SECRET'].encode(),
        raw_body,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, headers.get('X-Pronto-Signature', '')):
        return 401, {'error': 'invalid signature'}

    payload = json.loads(raw_body)
    event   = payload['type']
    data    = payload['data']['object']

    # 2. Your own record decides which user this is.
    row = find_verification(data['verification_token'])
    if not row:
        return 404, {'error': 'unknown verification'}

    approved = (
        event == 'age_verification.succeeded'
        and data['is_authentic']
        and data['is_adult']
    )

    set_age_verified(row['user_id'], approved)
    return 200, {'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/create-age-verification';
$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-API-Secret: ' . $API_SECRET,
]);

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-API-Secret. Do not send a platform identifier — it is resolved from your key.

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 — age_verification.succeeded
{
  "id":          "evt_6758afcccdafd2eb",
  "object":      "event",
  "api_version": "1.0",
  "created":     1735430400,
  "type":        "age_verification.succeeded",
  // The result fields are nested under data.object —
  // not at the top level.
  "data": {
    "object": {
      "verification_token":  "cf268392002f400a…",
      "platform_id":         "myapp_a1b2c3",
      "verification_status": "complete",
      "age_status":          "verified_adult",
      "is_adult":            true,        // met YOUR configured minimum_age
      "minimum_age":         18,          // the threshold that was applied
      "is_authentic":        true,        // document + liveness checks passed
      "age":                27,
      "client_reference":    "user_abc123",
      "state":               "a3f8c2d1e4b7",
      "completed_at":        1735430400
    }
  }
}
Reference

Tiers & pricing

FeatureBasicPremiumEnterprise
ID document scan
Biometric liveness check
Face matching
Selfie-only estimation
Document authentication Enterprise

Pricing

You choose who pays when you create the service. Both rates are net of tax — VAT is added at the applicable rate for the payer's country and shown before payment.

Billing modelRateWho pays
Platform-billed €0.35 / verification Invoiced to you monthly. Your users never see a payment step.
End-user-billed €0.99 / verification Each user pays ProntoID directly. No cost to your platform.
100 free verifications every month. Platform-billed services get 100 free per calendar month, per service — your age verification allowance is separate from any other ProntoID product you use. The allowance is shared across all platforms on your account and resets at the start of each UTC month. Usage is shown on your service's management page.

What counts as billable

A verification is billable once a result has been produced and the checks passed. You are not charged for failed verifications, and sessions a user abandons before a result are never counted — neither against your invoice nor against your free allowance.

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 / X-API-Secret, or the key has been revoked or has expired.
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.