Skip to content
ZeroKYC Pay

Webhooks reference

The full event catalogue with exact payloads, delivery headers and semantics, the 1m→12h retry ladder and consumer idempotency rules.

Everything about receiving ZeroKYC Pay events: the event catalogue, exact payload shapes, delivery headers, the retry ladder and idempotency rules.

Webhook endpoints are configured in the console under Webhooks: a URL plus a per-endpoint secret (whsec_…). Every active endpoint receives every event; an invoice can also override the URL ad hoc via the webhook_url field at creation.

Events

EventFires when
payment.detectedA transaction matching one of the invoice options is seen on chain (mempool or early confirmations)
payment.confirmedThe required confirmations are reached — the "ship the product" signal
payment.underpaidA payment arrived below the accepted range; the invoice becomes underpaid and needs your decision
invoice.expiredTTL elapsed without a valid payment
invoice.canceledThe invoice was canceled via POST /v1/invoices/{id}/cancel before payment
webhook.testYou pressed Send test event in the console — a signed smoke-check for your handler

invoice.created exists only in the event journal (GET /v1/events), it is not delivered to webhooks.

Multi-variant invoices (payment_currency=any) confirm on the first valid payment; the remaining options close automatically.

Payload

Every delivery is a JSON envelope: event id, type, created_at and the event-specific data object:

{
  "id": "evt_5c1a...",
  "type": "payment.confirmed",
  "created_at": "2026-09-06T14:02:33Z",
  "data": {
    "invoice_id": "inv_9f2k...",
    "order_id": "order-1042",
    "amount": "49.00",
    "base_currency": "USD",
    "option": {
      "asset": "TON",
      "network": "ton",
      "amount_crypto": "9.81",
      "paid_amount": "9.81"
    },
    "tx": {
      "hash": "9d24...",
      "confirmations": 1,
      "explorer_url": "https://tonviewer.com/transaction/9d24...",
      "output_index": 0
    },
    "metadata": { "customer_id": "cust_8821" }
  }
}

payment.detected, payment.confirmed and payment.underpaid share this exact shape. Field notes:

  • id is globally unique per event — store it and deduplicate (delivery is at-least-once).
  • order_id and metadata echo what you sent at invoice creation; order_id is your join key.
  • amount is the invoice total in base_currency; the crypto figures live in option (amount_crypto = due, paid_amount = actually observed on chain).
  • tx.explorer_url links the transaction on the asset's block explorer; output_index disambiguates UTXO/log outputs.
  • Overpayments confirm normally: option.paid_amount is the observed truth.

The lifecycle events are smaller:

{
  "id": "evt_7b2c...",
  "type": "invoice.expired",
  "created_at": "2026-09-06T20:20:11Z",
  "data": {
    "invoice_id": "inv_9f2k...",
    "order_id": "order-1042",
    "amount": "49.00",
    "base_currency": "USD"
  }
}

invoice.canceled carries only invoice_id and order_id; webhook.test carries endpoint_id, url and a message string.

Delivery headers

HeaderValue
Content-Typeapplication/json
User-AgentZeroKYC-Pay-Webhooks/1.0
X-ZKP-Signaturet=<unix seconds>,v1=<hex hmac-sha256>
X-ZKP-Event-IdSame as id in the payload — cheap dedup before parsing
X-ZKP-Event-TypeSame as type — route without parsing the body

The signature is HMAC-SHA256 over {timestamp}.{raw_body} with the endpoint secret; the timestamp window is ±300 seconds. Verify before trusting — full Node/PHP/Python snippets in HMAC verification.

Delivery semantics and retries

  • Success = any 2xx response within 10 seconds. Anything else — 4xx, 5xx, timeout, TLS failure — schedules a retry.
  • Redirects are not followed. A 3xx counts as a failure: point the endpoint at its final URL, not at a redirect.
  • Retry ladder:
AttemptDelay
1immediate
21 minute
35 minutes
430 minutes
52 hours
612 hours → then failed
  • When the ladder exhausts, the delivery is marked failed and a notification appears in the console. Manual retry (Webhooks → delivery log, or per-invoice) resets the attempt counter and fires immediately.
  • Rotate the endpoint secret and URL any time in Webhooks; the Send test event button delivers a signed webhook.test so you can verify the new config end-to-end.

Reconciliation

Your handlers should be idempotent, and the API gives you two ways to cross-check:

  1. GET /v1/invoices/{id} always returns the authoritative invoice state — treat webhooks as hints, not as the source of truth.
  2. GET /v1/events?limit=100 replays the event journal (including journal-only types like invoice.created), with type and created_after filters and cursor pagination — use it to backfill after downtime.

A rescan job re-checks all open invoices every 3 minutes, so chain events missed in real time self-heal: a payment.confirmed can arrive noticeably later than the transaction itself.

Consumer checklist

  1. Verify the X-ZKP-Signature HMAC and the ±300 s window before touching the payload.
  2. Deduplicate on event.id (or X-ZKP-Event-Id) — the same event may arrive more than once.
  3. Fulfil on payment.confirmed only; use payment.detected for early buyer feedback and payment.underpaid for manual review.
  4. Respond 2xx fast; do heavy work asynchronously.