Appearance
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:
| Header | Value |
|---|---|
content-type | application/json |
user-agent | Zay-Webhooks/1.0 |
webhook-id | UUID for this delivery. Stable across retries — use it to deduplicate |
webhook-timestamp | Unix seconds at signing time |
webhook-signature | Space-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."
}
}| Field | Description |
|---|---|
id | Same value as the webhook-id header — either works for deduplication |
type | The event, from the tables below |
occurred_at | When it happened, ISO 8601, UTC |
merchant_id | Your merchant id |
data | The 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.
type | Sent when | data |
|---|---|---|
user.onboarding_started | The user begins onboarding, on the web or in the app | user_id |
user.kyc_submitted | Identity verification was submitted and is under review | user_id |
user.kyc_passed | Identity verification was approved | user_id |
user.onboarding_completed | The user's card is issued and can spend — they can now be paid (account_state → active). Sent once per user | user_id, wallet_address |
user.onboarding_failed | Identity verification was declined for good — the user can never finish onboarding. Sent once per user | user_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.
type | Sent when | data |
|---|---|---|
payout.received | Zay confirmed the transfer (pending → received) | the Payout object |
payout.expired | Zay 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
type | Sent when | data |
|---|---|---|
webhook_endpoint.test | You call Trigger test delivery | webhook_endpoint_id, message |