Developers

Partner integration guide

Add age checks without collecting identity data. This page is generated from the canonical Markdown guide so engineers, LLMs, and agents all see the same contract.

All integrations: use the Partner session API, retrieve completed proofs from your backend with an API key, and verify every token before granting access.

Developer integration guide

Proof of Age lets partners add privacy-preserving age checks without becoming an identity database. Create a session, send the user to Proof of Age, and receive a signed, short-lived proof token.

Prove age, not identity. Partners receive only whether the user meets an age requirement — not their name, date of birth, passport details, email or wallet address.

This product is designed to align with UK and EU digital verification direction. It is not certified under any trust framework.

Getting access

  1. Choose a plan at proofofage.app/pricing and complete Stripe checkout.
  2. On the success page, save your API key, Partner ID, and webhook secret — they are shown once.
  3. Sign in to the partner dashboard with the billing email you used at checkout (magic link).

To explore the user experience before subscribing, try the live demo. The demo does not issue API credentials.

Your credentials

CredentialPurpose
API key (poa_…)Authenticate all Partner API requests: Authorization: Bearer <API_KEY>
Partner ID (partner_…)Proof token audience (aud claim). Used when verifying tokens — not for API authentication
Webhook secretVerify HMAC signatures on optional session callbacks

Store the API key and webhook secret securely. If you lose the API key, rotate it in the partner dashboard. If you lose the webhook secret, contact [email protected].

Your API base URL and JWKS URL are shown in the partner dashboard under Integration.

Partner ID in practice

Every proof token is scoped to your organisation. The JWT aud claim equals your Partner ID.

When using the verify API, confirm the response partnerId matches yours:

json
{
  "valid": true,
  "partnerId": "partner_a44ffac1-56ef-4353-b702-86d41f091f9c",
  "ageOver": 18
}

When verifying offline via JWKS, pass your Partner ID as the expected audience when validating the JWS signature (ES256). Reject tokens whose aud does not match.

Your Partner ID is also visible in the partner dashboard at any time. It is safe to store in configuration — unlike your API key, it is not a secret.

Quick start

Replace https://api.proofofage.app with your API base URL from the dashboard if different.

1. Create an age-check session

bash
curl -s -X POST https://api.proofofage.app/v1/age-check/sessions \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "requestedAge": 18,
    "returnUrl": "https://acme.example/age-verified"
  }'

Response:

json
{
  "sessionId": "acs_b852bd7e-6d7f-46e7-ab0d-5cb8a5473165",
  "status": "pending",
  "requestedAge": 18,
  "expiresAt": "2026-06-12T10:10:00.000Z",
  "verificationUrl": "https://proofofage.app/age-check/acs_b852bd7e-6d7f-46e7-ab0d-5cb8a5473165",
  "qrPayload": "https://proofofage.app/age-check/acs_b852bd7e-6d7f-46e7-ab0d-5cb8a5473165"
}

Redirect the user to verificationUrl, or render qrPayload as a QR code.

2. User completes verification

The user sees a consent screen listing exactly what will and will not be shared, then approves via:

  • Web / World App miniapp — World ID at /age-check/{sessionId}
  • Mobile app — scan QR or open proofofage://age-check/{sessionId}

On completion, if you provided returnUrl, the user is redirected with non-secret correlation data:

text
https://acme.example/age-verified?session_id=acs_b852bd7e-6d7f-46e7-ab0d-5cb8a5473165&status=approved

The query values are untrusted navigation hints. Never grant access from status; retrieve the session from your backend with your API key.

3. Retrieve the proof token

bash
curl -s https://api.proofofage.app/v1/age-check/sessions/<SESSION_ID> \
  -H "Authorization: Bearer <API_KEY>"

The authenticated response includes proof and proofToken when approved. The token is never returned to browser or mobile clients.

4. Verify the proof token

Option A — API verification (recommended):

bash
curl -s -X POST https://api.proofofage.app/v1/age-check/verify \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"token": "<PROOF_TOKEN>"}'

Response (success):

json
{
  "valid": true,
  "ageOver": 18,
  "proofLevel": "medium",
  "proofMethod": "world_id",
  "partnerId": "partner_a44ffac1-56ef-4353-b702-86d41f091f9c",
  "sessionId": "acs_b852bd7e-6d7f-46e7-ab0d-5cb8a5473165",
  "expiresAt": "2026-06-12T10:05:00.000Z"
}

Confirm partnerId matches your Partner ID before granting access.

Option B — offline verification via JWKS:

bash
curl -s https://api.proofofage.app/.well-known/proof-of-age/jwks.json

Verify the JWS locally with the public keys (ES256). Check aud matches your Partner ID.

5. Poll session status

bash
curl -s https://api.proofofage.app/v1/age-check/sessions/<SESSION_ID> \
  -H "Authorization: Bearer <API_KEY>"

Returns proof and proofToken when status is approved.

Backend integration snippet

tsx
// Server-side route in your application. Never send the API key to the browser.
async function createAgeCheck(apiKey: string) {
  const res = await fetch("https://api.proofofage.app/v1/age-check/sessions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      requestedAge: 18,
      returnUrl: "https://acme.example/age-verified",
    }),
  });

  return res.json();
}

// In your backend handler for /age-verified; sessionId comes from the query string.
async function handleReturn(apiKey: string, sessionId: string) {
  const res = await fetch(`https://api.proofofage.app/v1/age-check/sessions/${encodeURIComponent(sessionId)}`, {
    headers: { Authorization: `Bearer ${apiKey}` },
    cache: "no-store",
  });
  if (!res.ok) return { verified: false };

  const session = await res.json();
  if (session.status !== "approved" || !session.proofToken) return { verified: false };
  return { verified: true, ageOver: session.proof.ageOver };
}

What partners receive

FieldDescription
validWhether the proof is valid and unexpired
ageOverAge threshold met (e.g. 18)
proofLevellow, medium, or high assurance
proofMethodworld_id, passport_nfc, eu_wallet, etc.
partnerIdYour partner id (token audience)
sessionIdThe age-check session id
expiresAtProof expiry (short-lived, typically 5 minutes)

What partners never receive

  • Name
  • Date of birth
  • Passport or document number
  • Nationality
  • Email
  • Wallet address
  • Raw document or NFC data
  • Stable cross-session user identifiers

Proof tokens carry a session-scoped pseudonymous subject (sub) — not a user identity.

Usage and billing

Only successful verifications that result in an issued proof token count toward your plan. Abandoned sessions, user denials, and failed attempts are not billed.

Proof methodAssuranceMeter
World IDMediumMedium
Passport NFCHighHigh
EU walletHighHigh

View your usage for the current billing period in the partner dashboard. Overage proofs are billed via Stripe according to your plan.

Webhook callbacks

Optionally set callbackUrl when creating a session. The API POSTs a JSON payload on approve/deny with HMAC-SHA256 signing using your webhook secret.

Headers:

  • Content-Type: application/json
  • X-Proof-Of-Age-Signature: sha256=<hex>

Verify (Node.js):

javascript
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhook(body, signatureHeader, secret) {
  const expected = `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader ?? ""));
}

Callback URLs must use HTTPS in production. Private and metadata IP ranges are rejected (SSRF protection).

Privacy notes

  • Proofs are purpose-limited to one partner and one age threshold per session.
  • Only fixed age thresholds are supported: 13, 16, 18, 21, 25.
  • World ID zero-knowledge payloads are verified and discarded; raw nullifier hashes are never stored or returned.
  • Set includeRepeatSubject: true only when the user explicitly consents to a derived, partner-scoped repeat-use identifier.
  • Use HTTPS returnUrl values in production.

Further reading

For security review, DPIA support, or integration architecture questions, contact [email protected].

Support