Core API
Webhooks.
Signed HTTPS notifications, pushed the moment something happens orders settle, gates scan, refunds move. No polling, no missed state.
Most of ticketing happens after your HTTP request has come and gone: payment settles on the provider's clock, a ticket is scanned hours later at a door you don't control, a refund is approved overnight. Webhooks close that gap register an endpoint once and Zatabox POSTs you every state change as it happens, with a signature proving it came from us.
Register an endpoint#
Create endpoints in the portal or over the API, subscribing each one to exactly the events it cares about GET /api/v1/webhooks/catalog lists every type. The response includes the endpoint's signing secret (whsec_…), shown once at creation store it then. Send yourself a synthetic delivery with POST /api/v1/webhooks/{id}/test before anything real depends on it.
curl https://api.zatabox.com/api/v1/webhooks \ -H "Authorization: Bearer vt_live_…" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "url": "https://example.com/webhooks/zatabox", "events": ["order.completed", "ticket.checked_in", "refund.requested"] }'Verify signatures#
Every delivery carries an X-Zatabox-Signature header: a unix timestamp and an HMAC-SHA256, computed with your endpoint's signing secret over the string <timestamp>.<payload>. Verify against the raw request body parse the JSON only afterwards.
X-Zatabox-Signature: t=<unix seconds>,v1=<hex hmac-sha256>import express from 'express'const app = express() app.post('/webhooks/zatabox', express.raw({ type: 'application/json' }), // keep the raw bytes (req, res) => { const event = zatabox.webhooks.parseEvent( req.body, req.headers['x-zatabox-signature'], process.env.ZATABOX_WEBHOOK_SECRET, ) // throws on a bad signature beyond this line, trust the payload console.log(event.type, event.id) res.json({ received: true }) })import crypto from 'node:crypto' function verify(rawBody, header, secret) { const parts = Object.fromEntries( header.split(',').map((kv) => kv.split('=')), ) const expected = crypto .createHmac('sha256', secret) .update(parts.t + '.' + rawBody) .digest('hex') const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300 return ( fresh && crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) )}Delivery & retries#
- Delivery is at-least-once. Duplicates are rare but legal deduplicate on the event
id. - Anything but a 2xx is retried on an exponential backoff: 1m, 5m, 30m, 2h, 12h then daily, for 12 attempts in total before the delivery is marked exhausted.
- Replay any delivery from the dashboard, or with
POST /api/v1/webhooks/deliveries/{id}/replay. - Respond 2xx fast: acknowledge first, then do the work on a queue. Slow handlers read as failures and earn themselves a retry.
Event catalog#
All 27 event types, grouped by noun. The payload summary shows the top-level keys under data. One honest caveat: the three payout.* types are defined in the catalog but won't fire until payout execution launches balances accrue today, payout requests are rolling out.
| Field | Description | |
|---|---|---|
event.created | { event } | A draft event is created. |
event.updated | { event } | An event's details change. |
event.published | { event } | A draft goes live. |
event.cancelled | { event } | The organizer cancels the event. |
event.completed | { event } | The event ends. |
ticket_type.created | { ticketType } | A ticket type is added to an event. |
ticket_type.sold_out | { ticketType } | The last unit of a ticket type sells. |
order.created | { order } | Checkout begins; the order is pending. |
order.completed | { order, tickets } | Payment settles and tickets are minted. |
order.failed | { order } | Payment fails. |
order.cancelled | { order } | An unpaid order is cancelled. |
ticket.created | { ticket } | A ticket is issued. |
ticket.transferred | { ticket } | A ticket changes holder. |
ticket.checked_in | { ticket, checkin } | A gate scan succeeds. |
ticket.checkin_denied | { ticket } | A gate scan is rejected duplicate, wrong event, cancelled or expired. |
refund.requested | { refund } | A buyer asks for money back. |
refund.approved | { refund } | The organizer approves the request. |
refund.processed | { refund } | The money actually moves. |
refund.denied | { refund } | The organizer declines the request. |
report.submitted | { report } | A buyer files a report. |
report.resolved | { report } | The report is closed. |
message.received | { message } | A buyer messages the organizer. |
payout.scheduled | { payout } | A payout is queued. In the catalog now; fires once payouts launch. |
payout.paid | { payout } | Funds land in the organizer's account. Fires once payouts launch. |
payout.failed | { payout } | The transfer bounces. Fires once payouts launch. |
attendee.tagged | { attendee } | A tag is applied to an attendee. |
agent.action | { tool, args_hash, … } | An MCP tool performs a write see the MCP server docs. |
{ "id": "whe_01J…", "type": "order.completed", "created": "2026-06-10T14:02:11Z", "data": { "order": { "id": "ord_31xq", "total": "42.00", "currency": "USD" }, "tickets": [ { "id": "tic_…", "qr": "…" } ] }}