[View HTML version](/api/quickstart) · [Browse API reference](/api/v1) · [OpenAPI specification](/api/v1/openapi.yaml)

# Quickstart

Follow one verification from creation to its result.

## Before you start

Contact [contact@zeroage.org](mailto:contact@zeroage.org) for your credentials. You receive an API key and a webhook verification secret together. See [Pricing](https://zeroage.org/pricing/) for more information.

Every request requires sending your API key in the `Authorization` header. Requests with a missing or invalid API key return HTTP 401.

`Authorization: Bearer YOUR_API_KEY`

Use the webhook verification secret to respond to ZeroAge's endpoint ownership challenge. This confirms that you control the endpoint. Keep both credentials on your backend.

Configure the two shared destinations in [Webhooks and recovery](/api/webhooks.md) to receive completion and later status-change events. The guide covers endpoint verification and receiver setup. Webhooks are the expected notification path; you do not need to poll the event feed routinely.

## 1. Create a verification

Send this request from your backend. Use a new `Idempotency-Key` for each intended verification.

`evaluation_date` is optional in the request. When omitted, ZeroAge resolves it to the UTC civil date when the verification is first created. You can provide today or a future date in `YYYY-MM-DD` form. The resolved date never changes, including on exact retries after UTC midnight.

```sh
curl --request POST 'https://api.zeroage/v1/verifications' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: order-8f34d6f1-age-check' \
  --data '{
    "age_threshold": 18,
    "client_reference": "order-8f34d6f1"
  }'
```

## 2. Read the response

A successful creation returns HTTP 201 with a pending verification and the user's handoff URL:

```json
{
  "id": "ver_example_827",
  "client_reference": "order-8f34d6f1",
  "age_threshold": 18,
  "evaluation_date": "2026-10-08",
  "status": "pending",
  "created_at": "2026-10-08T10:00:00Z",
  "request_expires_at": "2026-10-08T10:15:00Z",
  "revision": 0,
  "original": null,
  "current": null,
  "verification_url": "https://verify.zeroage.example/start/example-handoff"
}
```

Save the verification `id` to match later events. `original` and `current` are null until the request becomes terminal.

## 3. Send the user to the check

Direct the user to `verification_url` to complete the check in the mobile app before `request_expires_at`. Treat the URL as sensitive bearer access: do not log it or put it in `client_reference`.

Only creation and exact creation retries return this URL. Lookup responses and events omit it.

## 4. Read the completion event

The completion destination receives a `verification.completed` event:

```json
{
  "id": "evt_example_completion_827",
  "type": "verification.completed",
  "created_at": "2026-10-08T10:02:00Z",
  "verification_id": "ver_example_827",
  "revision": 1,
  "data": {
    "id": "ver_example_827",
    "client_reference": "order-8f34d6f1",
    "age_threshold": 18,
    "evaluation_date": "2026-10-08",
    "status": "completed",
    "created_at": "2026-10-08T10:00:00Z",
    "request_expires_at": "2026-10-08T10:15:00Z",
    "revision": 1,
    "original": {
      "result": "yes",
      "reason": "verified",
      "evaluated_at": "2026-10-08T10:02:00Z"
    },
    "current": {
      "result": "yes",
      "review_status": "clear",
      "reasons": [],
      "assessed_at": "2026-10-08T10:02:00Z"
    }
  }
}
```

After validating the event, read `data.original.result` as `yes`, `no`, or `unsure`. A failed, cancelled, expired, or unsupported check produces `unsure`; it does not establish that the user is under the age threshold. See [Results and revisions](/api/results.md).

Deduplicate event IDs and compare revisions. Later `verification.status_changed` events report changes to the current assessment without changing the original decision. See [Webhooks and recovery](/api/webhooks.md) for receiver setup, ordering, and recovery, and [Integration testing](/api/testing.md) for checks to cover. Detailed requests and schemas are in the [API reference](/api/v1).

## Errors and retries

API errors use an `error` object with a stable `code` and a safe `message`. Branch on `error.code`, not `message`. The message is for display only and never contains submitted values, credentials, secrets, or document data. See the [API reference](/api/v1) for the JSON error envelope and response examples.

| HTTP status | Error code | Action |
| --- | --- | --- |
| 400 | `invalid_request` | Correct the request input before sending it again. |
| 401 | `unauthorized` | Stop automatic retries and correct the API credentials. |
| 404 | `not_found` | Check the verification ID and customer credentials. An unknown verification and a verification owned by another customer are indistinguishable. |
| 409 | `idempotency_conflict` | Reconcile the changed creation request with the original request. Do not automatically create a new idempotency key. |
| 429 | `rate_limited` | Respect `Retry-After` when supplied. Otherwise, use bounded backoff with jitter. |
| 503 | `service_unavailable` | Retry with bounded backoff and jitter. |

An API error is never an age result. This also applies to an unknown error code: do not infer `yes`, `no`, or `unsure` from it.

A creation timeout or HTTP 503 can leave the outcome unknown. Retry with the same `Idempotency-Key` and the same request body. If `evaluation_date` was omitted, keep it omitted, including after UTC midnight. Never respond to an uncertain outcome by blindly minting a new key.

For a read-only request, retry the same query and cursor. After an uncertain timeout or service error from `PUT /v1/webhook-endpoints`, read the current configuration and reconcile it before resubmitting. Do not assume whether an endpoint ownership challenge will repeat.

Keep every automatic retry sequence bounded and use backoff with jitter.
