Skip to content

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 ​

RequirementDescription
A public HTTPS URLZay refuses http, localhost and private addresses
A registered endpoint and its signing_secretFrom Register webhook endpoint — the secret is shown only once

How it works ​

  1. Verify the signature
  2. Deduplicate
  3. Respond, then process
  4. Test it

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 on

Python ​

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 ​

  1. Read the raw request body as bytes. Whitespace and key order matter.
  2. Read webhook-id, webhook-timestamp and webhook-signature. Reject with 400 if any is missing.
  3. Check freshness. Reject if webhook-timestamp is more than ±5 minutes from your clock.
  4. Build the string to sign, joined with literal dots:
    {webhook-id}.{webhook-timestamp}.{raw body}
  5. Derive the key. Strip the whsec_ prefix from your secret and base64-decode the rest.
  6. Compute HMAC-SHA256(key, string-to-sign) and base64-encode it (standard alphabet, with padding).
  7. Compare. webhook-signature is a space-separated list of vN,<base64> entries. Accept if any v1 entry 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.