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

# Webhooks and recovery

Webhooks are the expected path for receiving verification completion and the rare later status change. A webhook integration does not need to poll `GET /v1/events` during normal operation.

## Configure destinations

Configure both destinations with `PUT /v1/webhook-endpoints`, which replaces the complete pair. Set either value to `null` to disable that stream. The configuration is shared across your verifications. Use `GET /v1/webhook-endpoints` when you want to read the current configuration; it is not a required setup step.

When you configure a destination, ZeroAge sends a challenge to that endpoint. Your endpoint uses your webhook verification secret to produce the challenge response. A correct response confirms that you control the endpoint. Keep the secret on your backend.

| Destination | Event |
| --- | --- |
| `verification_completed_url` | `verification.completed`: one logical event for each terminal request, including cancelled and expired requests whose result is `unsure` |
| `verification_status_changed_url` | `verification.status_changed`: a later material change to the current assessment |

Delivery can repeat an event or deliver events out of order. A later material change has a new revision; unchanged reassessment does not produce an event.

## Receive events

The `data` field is a full base Verification snapshot at that revision. It excludes the sensitive handoff URL, app capabilities, signatures, proofs, and evidence. See the [Quickstart](/api/quickstart.md) and [API reference](/api/v1) for complete JSON examples.

1. Validate the event schema and require `verification_id == data.id` and `revision == data.revision` before starting business processing.
2. Deduplicate durably by event `id`. Acknowledge an identical duplicate. If the same ID has conflicting content, stop processing and reconcile; never overwrite the saved event.
3. Save the event durably before returning HTTP 200, 204, or another 2xx response, then process it asynchronously. Apply the [Results and revisions](/api/results.md) revision rules; reconcile conflicting original decisions without overwriting `original`. Commit the state update and job enqueue atomically. Make business effects idempotent.
4. Resume queued work after a worker restart.

A 2xx response marks the event as received. It does not mean that downstream business processing has completed.

ZeroAge retries automatically after a timeout or non-2xx response, with increasing delays. Every attempt for one logical event uses the same event ID and payload, so keep the duplicate and ordering safeguards above.

## Optional event feed

`GET /v1/events` is an optional recovery and troubleshooting tool, and an alternative for customers that choose polling instead of webhooks. Webhook users do not need to poll it routinely.

1. Call `GET /v1/events`; omit `after` on the first request. Treat every returned cursor as opaque and keep each customer's credentials, events, and cursor separate.
2. If you use the feed, save all events durably before advancing to `next_cursor`. If `has_more` is true, request the next page immediately. Otherwise retain that cursor for the next poll, including when the page is empty.
3. Reconcile uncertain state with `GET /v1/verifications/{id}`. If local delivery may be stale, read the current Verification before a consequential action.
4. Apply the same revision and original-decision rules used for webhook delivery.
