Appearance
Receive and verify webhooks
Build an endpoint that accepts Zay's events, checks they really come from Zay, and handles each one once.
The envelope and headers are described in the Event catalog; retries and disabling in How webhook delivery works.
Requirements
| Requirement | Description |
|---|---|
| A public HTTPS URL | Zay refuses http, localhost and private addresses |
A registered endpoint and its signing_secret | From Register webhook endpoint — the secret is shown only once |
How it works
1. Verify the signature
Deliveries follow Standard Webhooks, which ships verified SDKs for Ruby, Python, Node.js, Go, PHP and others. Pass them the raw request body — not a parsed and re-serialized one — and the headers:
Ruby
ruby
require "standardwebhooks"
wh = StandardWebhooks::Webhook.new(ENV["ZAY_WEBHOOK_SIGNING_SECRET"])
payload = wh.verify(request.raw_post, request.headers)
# payload is the parsed JSON body — safe to act onPython
python
from standardwebhooks import Webhook
wh = Webhook(os.environ["ZAY_WEBHOOK_SIGNING_SECRET"])
payload = wh.verify(request_body_bytes, request_headers)Node.js
javascript
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.ZAY_WEBHOOK_SIGNING_SECRET);
const payload = wh.verify(requestBody, requestHeaders);The SDK checks the signature in constant time and rejects a webhook-timestamp more than 5 minutes off. If verification fails, respond 400 and stop.
Without an SDK
- Read the raw request body as bytes. Whitespace and key order matter.
- Read
webhook-id,webhook-timestampandwebhook-signature. Reject with400if any is missing. - Check freshness. Reject if
webhook-timestampis more than ±5 minutes from your clock. - Build the string to sign, joined with literal dots:
{webhook-id}.{webhook-timestamp}.{raw body} - Derive the key. Strip the
whsec_prefix from your secret and base64-decode the rest. - Compute
HMAC-SHA256(key, string-to-sign)and base64-encode it (standard alphabet, with padding). - Compare.
webhook-signatureis a space-separated list ofvN,<base64>entries. Accept if anyv1entry matches your digest, using a constant-time comparison.
2. Deduplicate
Zay delivers each event at least once, so the same event can arrive twice. Record each webhook-id (the same value as the envelope id) — for example in a table with a unique index — and skip any you've already handled. Zay sends the same webhook-id on every retry of a delivery.
Events can also arrive out of order. Treat each one as a statement about state at its occurred_at, and when in doubt, read the user or payout from the API.
3. Respond, then process
Return a 2xx as soon as the event is verified and stored, and do the work afterwards. Zay waits up to 30 seconds; anything slower, or any other status, counts as a failure and is retried. Sustained failures disable the endpoint.
Ignore any type you don't recognize, with a 2xx — new event types are added over time.
4. Test it
Ask Zay to send a test event with Trigger test delivery. The endpoint must be enabled.
Check the result
Your endpoint received a webhook_endpoint.test event, verified it, and answered 2xx. Retrieve webhook endpoint still shows status: enabled.
Related
- Event catalog — every event and its payload
- How webhook delivery works — retries, ordering and disabling
- Webhook endpoints — register, update and test endpoints