# 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](https://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](https://proofofage.app/partner/login) with the **billing email** you used at checkout (magic link).

To explore the user experience before subscribing, try the [live demo](https://proofofage.app/demo). The demo does not issue API credentials.

## Your credentials

| Credential | Purpose |
| --- | --- |
| **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 secret** | Verify 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 [hello@proofoflife.app](mailto:hello@proofoflife.app).

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 approval, if you provided `returnUrl`, the user is redirected with:

```
https://acme.example/age-verified?proof_token=<signed-jws>
```

### 3. 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.

### 4. Poll session status (alternative)

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

## React integration snippet

```tsx
const API_BASE = "https://api.proofofage.app";

async function startAgeCheck(apiKey: string) {
  const res = await fetch(`${API_BASE}/v1/age-check/sessions`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      requestedAge: 18,
      returnUrl: `${window.location.origin}/age-verified`,
    }),
  });

  const { verificationUrl } = await res.json();
  window.location.href = verificationUrl;
}

// On your /age-verified page:
async function handleReturn(apiKey: string, expectedPartnerId: string) {
  const token = new URLSearchParams(window.location.search).get("proof_token");
  if (!token) return { verified: false };

  const res = await fetch(`${API_BASE}/v1/age-check/verify`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ token }),
  });

  const result = await res.json();
  if (!result.valid || result.partnerId !== expectedPartnerId) {
    return { verified: false };
  }
  return { verified: true, ageOver: result.ageOver };
}
```

## What partners receive

| Field | Description |
| --- | --- |
| `valid` | Whether the proof is valid and unexpired |
| `ageOver` | Age threshold met (e.g. 18) |
| `proofLevel` | `low`, `medium`, or `high` assurance |
| `proofMethod` | `world_id`, `passport_nfc`, `eu_wallet`, etc. |
| `partnerId` | Your partner id (token audience) |
| `sessionId` | The age-check session id |
| `expiresAt` | Proof 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 method | Assurance | Meter |
| --- | --- | --- |
| World ID | Medium | Medium |
| Passport NFC | High | High |
| EU wallet | High | High |

View your usage for the current billing period in the [partner dashboard](https://proofofage.app/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

- [Privacy Policy](https://proofofage.app/privacy) — how verification data is handled, including partner age-check sessions
- [Terms of Service](https://proofofage.app/terms) — service terms for partners and users

For security review, DPIA support, or integration architecture questions, contact [hello@proofoflife.app](mailto:hello@proofoflife.app).

## Support

- Partner dashboard: [proofofage.app/partner/login](https://proofofage.app/partner/login)
- Email: [hello@proofoflife.app](mailto:hello@proofoflife.app)
