Get a signed HTTP POST to your own server (or Zapier / Make) when things happen in your organization — so you can sync a CRM, trigger fulfillment, or update a dashboard in real time. Add an endpoint under Developers → Webhooks in your organizer dashboard.
| Event | Sent when |
|---|---|
event.published | An event moves from draft to published. |
order.paid | A checkout completes (paid or free RSVP). Carries order id, event id, amount, currency, and buyer name/email. |
order.refunded | A paid order is refunded. Same shape as order.paid plus the refund id, whether it was recorded manually, and how many tickets were revoked. |
Subscribe an endpoint to specific events, or leave all boxes unchecked to receive every event. More events are on the way.
Every delivery is a POST with a JSON body and these headers:
X-VenueFuze-Event — the event type, e.g. order.paid.X-VenueFuze-Delivery-Id — a stable id for this delivery; use it for idempotency.X-VenueFuze-Timestamp — the Unix time (seconds) the signature was computed.X-VenueFuze-Signature — t=<unix>,v1=<hmac> (see below).{
"id": "whd_9f3c…", // delivery id — use for idempotency
"type": "order.paid",
"version": "1",
"createdAt": "2026-09-17T15:04:05.000Z",
"data": {
"id": "ord_…",
"eventId": "evt_…",
"totalCents": 4500,
"currency": "usd",
"buyerName": "Alex Rivera",
"buyerEmail": "alex@example.com",
"provider": "STRIPE",
"paidAt": "2026-09-17T15:04:05.000Z"
}
}Compute HMAC-SHA256(secret, "{t}.{rawBody}") and compare it, in constant time, to the v1 value in X-VenueFuze-Signature. Reject the request if t is more than five minutes old (replay defense). Always use the raw request bytes — re-serializing parsed JSON will change the body and break the signature.
import crypto from "node:crypto";
// Your endpoint's signing secret (starts with "whsec_"), shown once when
// you add the endpoint. Store it as a secret, never in client code.
const SECRET = process.env.VENUEFUZE_WEBHOOK_SECRET;
/** Express-style handler. Give it the RAW request body (not parsed JSON). */
function handleWebhook(req, res) {
const header = req.header("X-VenueFuze-Signature") || "";
const rawBody = req.rawBody; // a Buffer/string of the exact bytes received
// Parse "t=<unix>,v1=<hex>"
const parts = Object.fromEntries(
header.split(",").map((kv) => {
const i = kv.indexOf("=");
return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
}),
);
const t = Number(parts.t);
const provided = parts.v1;
if (!Number.isFinite(t) || !provided) return res.sendStatus(400);
// Reject replays: timestamp must be within 5 minutes.
if (Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);
// Recompute HMAC-SHA256 over "<t>.<rawBody>" and compare in constant time.
const expected = crypto.createHmac("sha256", SECRET).update(t + "." + rawBody).digest("hex");
const a = Buffer.from(provided, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(400);
// Verified. Use X-VenueFuze-Delivery-Id for idempotency, then respond 2xx fast.
const event = JSON.parse(rawBody);
// ... do your work (enqueue, don't block) ...
res.sendStatus(200);
}2xx within 10 seconds. Do your real work asynchronously — don't block the response.2xx (or a timeout) is retried with backoff at 1, 5, 15, 30, 60, then 120 minutes — up to 6 attempts.X-VenueFuze-Delivery-Id, so key your processing on it to stay idempotent.