Search the academy
Search articles by title or content
Browse for event organizers
Receive webhooks
Add a webhook endpoint in Settings → Developers or over the API, verify the Standard Webhooks signature in Node.js, and understand retries, redelivery, and auto-disable.
Webhooks push signed HTTPS POSTs to your server the moment something happens — an order lands, a ticket checks in, an event goes live — so you never have to poll.
Add an endpoint
In the dashboard (Admin role or above), go to Settings → Developers and open the Webhooks tab.
- Choose Add endpoint.
- Enter the Endpoint URL — a public HTTPS URL; private and internal addresses are rejected.
- Optionally add a Description, pick the Events to send (at least one), and choose Add endpoint.
- Copy the
whsec_...signing secret and store it with your integration — you can Reveal or Rotate it later from the endpoint page.
Or do the same over the API: POST /v1/webhook_endpoints with url, optional description, and event_types returns the endpoint with its raw secret once — every later read shows only a secret_preview. Manage endpoints with GET/PATCH/DELETE /v1/webhook_endpoints/..., rotate with POST /v1/webhook_endpoints/.../rotate_secret (the old secret stops verifying immediately; the new one is in that response only), and read the delivery log with GET /v1/webhook_endpoints/.../deliveries.
The event types
| Type | Fired when |
| --- | --- |
| order.created | An order was placed (storefront, box office, kiosk, or API) |
| order.paid | A previously-unpaid order was settled in full (orders paid at creation emit only order.created) |
| order.refunded | A refund was issued |
| ticket.issued | E-tickets were generated and sent to the buyer |
| ticket.checked_in | A ticket was admitted at the door |
| event.published | A performance went on sale |
| event.updated | An existing event's details were changed (drafts included) |
Each delivery POSTs a JSON body of the form type + timestamp + data, where data is a compact summary — ids plus a few headline fields. Fetch the full resource from the API when you need more. A real order.created body:
{
"type": "order.created",
"timestamp": "2026-07-06T11:03:58.954Z",
"data": {
"order_id": "84a40659-a5b7-4eae-9e8a-03be476fd58f",
"order_no": "ZNR-8NZFPRZ8",
"status": "unpaid",
"channel": "boxOffice",
"currency": "USD",
"total_minor": 14887
}
}
Verify the signature
Deliveries follow the Standard Webhooks convention, with three headers: webhook-id (stable for the event across every retry and redelivery — your dedup key), webhook-timestamp (unix seconds, fresh per attempt), and webhook-signature (v1, followed by a base64 HMAC). Recompute HMAC-SHA256 over the string id.timestamp.body using the base64-decoded part of your secret after the whsec_ prefix. A complete receiver:
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
const secret = process.env.ZUNARO_WHSEC; // "whsec_..." from endpoint creation
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const TOLERANCE_S = 5 * 60;
createServer((req, res) => {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
const id = req.headers["webhook-id"];
const ts = req.headers["webhook-timestamp"];
const sig = req.headers["webhook-signature"] ?? "";
const expected = `v1,${createHmac("sha256", key)
.update(`${id}.${ts}.${body}`)
.digest("base64")}`;
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= TOLERANCE_S;
let valid = false;
try {
valid = fresh && timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
} catch {
valid = false;
}
if (!valid) {
res.writeHead(401).end();
return;
}
const event = JSON.parse(body);
// Dedupe on the webhook-id header: retries and redeliveries reuse it.
console.log(event.type, event.data);
res.writeHead(200).end();
});
}).listen(3000);
Retries and auto-disable
- Any 2xx within 15 seconds counts as delivered. Redirects are not followed — a 3xx is a failure.
- A failed attempt retries up to six times, backing off 30s, 2m, 10m, 1h, 6h, then 24h (about 31 hours in total).
- Respond 410 Gone to unsubscribe: the endpoint is auto-disabled immediately and nothing more is sent.
- An endpoint that keeps failing across many events for days is auto-disabled; the dashboard shows Auto-disabled. Fix the receiver, then Enable it again (or PATCH its
statustoenabledover the API). - The endpoint page lists Recent deliveries with status, HTTP code, and latency; Redeliver (or
POST /v1/webhook_deliveries/.../redeliver) queues a fresh attempt with a new retry budget — under the samewebhook-id, so your dedup still holds.