---
name: zetaid-integration
description: Integrate ZetaID identity verification into a website or app backend — create verifications with the API key, send the person to the verification page, receive and verify signed webhooks, read results, handle every status. Use when the user asks to add ZetaID, identity/age verification, KYC, or ID checks with ZetaID to their project.
---

# ZetaID integration

You are adding ZetaID identity verification to the user's project. The full
documentation is at https://docs.zetaid.com (machine-readable:
https://docs.zetaid.com/llms-full.txt, OpenAPI: https://docs.zetaid.com/openapi.yaml).
Read the parts you need; the rules below are not optional.

## The model

1. The user's **server** creates a verification:
   `POST https://verify.zetaid.com/api/v1/verifications` with header
   `X-SDK-Key: <API key>` and JSON `{return_url?, extra_data?, flow?, module_overrides?}`.
   Answer `201`: `{verification_id, url, flow, due_at, expires_at}`.
2. The person's browser is **redirected to `url`** (top-level page, never an
   iframe; in mobile apps an in-app browser tab, not a WebView).
3. ZetaID posts **signed webhooks** to the key's webhook URL:
   `verification.completed` (`status` verified | rejected |
   needs_manual_review, with `export`), `verification.reviewed` (manual review
   decided: verified | rejected), `verification.expired` (`reason_code`
   COMPLETION_TIMEOUT | ABANDONED).
4. The server can read a verification any time:
   `GET https://verify.zetaid.com/api/v1/verifications/{id}` with `X-SDK-Key` →
   `{verification_id, status, terminal, reason_code, export, extra_data, created_at, updated_at}`.
5. The person comes back to `return_url` (nothing is appended to it).

## Hard rules

- **Secrets stay on the server.** Read the API key (`vk_live_…`) and the
  webhook signing secret (`whsec_…`) from environment variables or the
  project's secret store (e.g. `ZETAID_API_KEY`, `ZETAID_WEBHOOK_SECRET`).
  Never put them in client-side code, mobile apps, or the repository. The API
  refuses browser requests (`403 SERVER_SIDE_ONLY`).
- **Verify every webhook signature** before acting:
  `X-Webhook-Signature` must equal `"sha256=" + hex(HMAC_SHA256(secret, X-Webhook-Timestamp + "." + rawBody))`,
  compared in constant time, over the **raw** request body (configure the
  framework so this route gets the unparsed body), and the timestamp must be
  within 300 seconds of now. Answer `401` and do nothing otherwise.
- **The server decides.** Never treat the return to `return_url`, a query
  parameter or anything from the browser as proof of verification. Decide
  from a verified webhook or from the server's own `GET`.
- **Tie each verification to one record** (order, booking, account): store
  `verification_id` on it and put the record's id in `extra_data` and in
  `return_url`. Ignore webhooks whose `verification_id` is not the record's
  current one (an older attempt reporting late).
- **Idempotent webhooks.** Answer `2xx` within 20 seconds, do slow work
  after. Retries repeat the same `X-Webhook-Delivery`; repeats must not
  change anything twice.
- **No personal data in `extra_data`.** Only identifiers.
- **Keep only what the user's product needs** from `export` (e.g. only
  `personal_info.date_of_birth` for an age check). Don't log the export.
- **Handle every status**: verified → continue; rejected → explain, allow a
  new attempt (new verification); needs_manual_review → tell the person it
  will be confirmed, wait for `verification.reviewed`; expired → offer to
  start again; anything else → still pending.
- **Errors**: retry only 5xx (`503 SYSTEM_TEMPORARILY_UNAVAILABLE`,
  `502 BACKEND_UNREACHABLE`) with backoff; `402 CREDITS_EXHAUSTED` means the
  prepaid balance is used up; 4xx are bugs or configuration — don't retry.
- **Don't invent endpoints.** The API has exactly the two calls above. There
  is no endpoint to list verifications, change keys or settings, or delete
  data: that is done in the operator console by the user.

## What to build (typical web app)

1. A server route that creates the verification for the current record,
   stores `verification_id` + status `pending`, and redirects (303) to `url`.
2. A webhook route (raw body) that verifies the signature, maps `status`
   onto the record, and answers 204.
3. A return route that loads the record; if still pending, calls `GET` once
   and updates it; then continues (verified), or shows waiting / review /
   failed / expired screens. The waiting screen refreshes every few seconds
   and gives up after about a minute with a "we'll notify you" message.
4. Configuration: `ZETAID_API_KEY`, `ZETAID_WEBHOOK_SECRET`, optionally
   `ZETAID_API_BASE` (default `https://verify.zetaid.com/api/v1`).
5. Tests: signature verification (valid, wrong secret, old timestamp,
   modified body), status mapping, a late webhook of an older attempt.

## What the user does in the operator console

Tell the user (you can't do this for them):
- **API keys → New API key**: name, webhook URL (the route from step 2, on
  HTTPS), flow (e.g. document then liveness). The API key and the webhook
  signing secret are shown once — they put them into the secret store.
- Retention of images and personal data per key, matching their obligations.
- Manual reviews are handled under **Verifications**.

## Reference code (Node.js)

```js
import crypto from "node:crypto";

export function validWebhook(secret, headers, rawBody) {
  const ts = headers["x-webhook-timestamp"];
  const sig = headers["x-webhook-signature"] ?? "";
  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest("hex");
  return sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
```

Python: `hmac.compare_digest("sha256=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest(), signature)`.
PHP: `hash_equals('sha256=' . hash_hmac('sha256', $ts . '.' . $rawBody, $secret), $sig)`.

A complete example (Express): https://docs.zetaid.com/guides/website-integration/
