Skip to content

Event catalog ​

Every event Zay sends to your webhook endpoints, and what it carries.

How to check that an event really comes from Zay: Receive and verify webhooks. Retries, ordering and disabling: How webhook delivery works.

Delivery format ​

Zay POSTs each event to your url with these headers:

HeaderValue
content-typeapplication/json
user-agentZay-Webhooks/1.0
webhook-idUUID for this delivery. Stable across retries — use it to deduplicate
webhook-timestampUnix seconds at signing time
webhook-signatureSpace-separated list of vN,<base64> entries. Today only v1 is sent; the list leaves room for secret rotation

The body is a JSON envelope:

json
{
  "id": "0192f5b0-1234-7e8d-89ab-cdef01234567",
  "type": "webhook_endpoint.test",
  "occurred_at": "2025-12-05T12:05:49Z",
  "merchant_id": "2c9596e6-7c48-4fe9-b957-738bd2e04adc",
  "data": {
    "webhook_endpoint_id": "0192f5a4-7c48-4fe9-b957-738bd2e04adc",
    "message": "This is a test event from Zay. If you received this, your endpoint is configured correctly."
  }
}
FieldDescription
idSame value as the webhook-id header — either works for deduplication
typeThe event, from the tables below
occurred_atWhen it happened, ISO 8601, UTC
merchant_idYour merchant id
dataThe event's payload, described per event below

Naming ​

Event names follow one convention: <entity>.<event>, where entity is the resource the event is about (user, payout, webhook_endpoint) and event is a past-tense, snake_case description of what happened — e.g. user.onboarding_completed, payout.received.

New event types are added over time, and adding one is always backward-compatible. Treat any type you don't recognize as a no-op rather than an error, so new events never break your receiver.

User events ​

Sent as a user moves through onboarding. Every payload carries user_id — the user's id, as returned by Create user.

typeSent whendata
user.onboarding_startedThe user begins onboarding, on the web or in the appuser_id
user.kyc_submittedIdentity verification was submitted and is under reviewuser_id
user.kyc_passedIdentity verification was approveduser_id
user.onboarding_completedThe user's card is issued and can spend — they can now be paid (account_state → active). Sent once per useruser_id, wallet_address
user.onboarding_failedIdentity verification was declined for good — the user can never finish onboarding. Sent once per useruser_id, reason

user.onboarding_completed and user.onboarding_failed are the two endings, and the events to act on. The others are progress signals — subscribe to them if you show onboarding progress to your users.

A verification the user can retry — a mistyped detail, a blurry photo — sends no event: the user fixes it and carries on. Only a final refusal sends user.onboarding_failed. A user who simply stops stays in account_state onboarding and sends nothing — see How onboarding works.

Example — user.onboarding_completed:

json
{
  "id": "0192f5b0-1234-7e8d-89ab-cdef01234567",
  "type": "user.onboarding_completed",
  "occurred_at": "2025-12-05T12:05:49Z",
  "merchant_id": "2c9596e6-7c48-4fe9-b957-738bd2e04adc",
  "data": {
    "user_id": "8775c532-9571-4c70-8bcd-8a822b9a0d9a",
    "wallet_address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb7"
  }
}

wallet_address is where you send the user's payouts. The event is sent the moment the card can spend: after it is issued, it is activated, allowing it to spend from the user's wallet, and the event follows once that activation is on chain — whether the web onboarding activates it, which it does without asking the user, or the Zay app does.

Example — user.onboarding_failed:

json
{
  "id": "0192f5b0-5678-7e8d-89ab-cdef01234567",
  "type": "user.onboarding_failed",
  "occurred_at": "2025-12-05T11:58:02Z",
  "merchant_id": "2c9596e6-7c48-4fe9-b957-738bd2e04adc",
  "data": {
    "user_id": "8775c532-9571-4c70-8bcd-8a822b9a0d9a",
    "reason": {
      "code": "region_not_supported"
    }
  }
}

reason.code says why, and is one of:

  • region_not_supported — Zay cannot serve the user's country. Offer them another way to be paid.
  • rejected_by_provider — the identity provider refused the user, for a reason it does not share.

The list may grow: treat a code you don't recognize as rejected_by_provider. The user's account_state stays onboarding.

Payout events ​

Sent when Zay settles a payout you reported. data is the full Payout object in its new status, so you can act without a follow-up GET.

typeSent whendata
payout.receivedZay confirmed the transfer (pending → received)the Payout object
payout.expiredZay could not confirm the transfer (pending → expired)the Payout object, with reason set

reason on payout.expired is one of:

  • deposit not confirmed within the verification window — no matching deposit arrived within 48 hours of the report;
  • deposit did not match the reported payout (wrong amount, token, or failed transaction).

Reporting a payout is not an event — you call Report a payout yourself and get the result synchronously.

Example — payout.received:

json
{
  "id": "0192f5b0-1234-7e8d-89ab-cdef01234567",
  "type": "payout.received",
  "occurred_at": "2025-12-05T12:05:49Z",
  "merchant_id": "2c9596e6-7c48-4fe9-b957-738bd2e04adc",
  "data": {
    "id": "60eb2155-c3b4-43d7-9451-0fe1cc009d85",
    "user_id": "8775c532-9571-4c70-8bcd-8a822b9a0d9a",
    "amount": "100.0",
    "currency_code": "USDC",
    "status": "received",
    "transaction_hash": "0xabc123",
    "reason": null,
    "custom_metadata": {"invoice_ref": "INV-42"},
    "created_at": "2025-12-05T12:05:49Z"
  }
}

Example — payout.expired:

json
{
  "id": "0192f5b0-90ab-7cde-89ab-cdef01234567",
  "type": "payout.expired",
  "occurred_at": "2025-12-07T12:06:00Z",
  "merchant_id": "2c9596e6-7c48-4fe9-b957-738bd2e04adc",
  "data": {
    "id": "60eb2155-c3b4-43d7-9451-0fe1cc009d85",
    "user_id": "8775c532-9571-4c70-8bcd-8a822b9a0d9a",
    "amount": "100.0",
    "currency_code": "USDC",
    "status": "expired",
    "transaction_hash": "0xabc123",
    "reason": "deposit not confirmed within the verification window",
    "custom_metadata": {"invoice_ref": "INV-42"},
    "created_at": "2025-12-05T12:05:49Z"
  }
}

Operational events ​

typeSent whendata
webhook_endpoint.testYou call Trigger test deliverywebhook_endpoint_id, message