Developer docs

Webhooks

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.

Events

EventSent when
event.publishedAn event moves from draft to published.
order.paidA checkout completes (paid or free RSVP). Carries order id, event id, amount, currency, and buyer name/email.
order.refundedA 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.

Request format

Every delivery is a POST with a JSON body and these headers:

{
  "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"
  }
}

Verifying signatures

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);
}

Retries & idempotency

Set up an endpoint →