Docs

Webhooks

When seats are booked or released, holds expire or a venue publishes a new chart, venuery POSTs a signed notification to your endpoints. Prefer polling? The same notifications are in a feed.

Add an endpoint

curl https://www.venuery.com/v1/webhook_endpoints -u sk_live_…: \
  -d '{"url":"https://tickets.example.com/hooks/venuery","events":["objects.booked","hold.expired"]}'

The response has the endpoint's signing secret (whsec_…), shown this once. Endpoints made with a test key get test-mode notifications only. Leave out events to get every type.

What you receive

POST https://tickets.example.com/hooks/venuery
venuery-signature: t=1779991506,v1=5f0c4e…
venuery-notification-id: ntf_812
content-type: application/json

{
  "id": "ntf_812",
  "object": "notification",
  "type": "objects.booked",
  "created_at": "2027-05-20T18:05:06.000Z",
  "livemode": true,
  "data": {
    "event": "evt_9q1x7c4b0d8f3k2m",
    "action": "book",
    "objects": ["s_4k9q2m8f1x7c"],
    "ga": [],
    "order_ref": "ORD-1042",
    "hold": "hld_x7c4b0d8f3k2m9q1",
    "metadata": { "email": "[email protected]" }
  }
}
TypeWhendata
hold.createdA hold was made through the API or the seat map.hold, event, objects, ga, expires_at
hold.expiredA hold ran out before it was booked. Its seats are back on sale.hold, event, objects
objects.bookedObjects or GA places were booked.event, objects, ga, order_ref, hold, metadata
objects.releasedBooked places were freed.event, objects, ga, order_ref
objects.status_changedPlaces were blocked or unblocked.event, action, objects, ga, status
season.bookedSeats were booked for a whole season.season, objects, ga, order_ref, events, metadata
season.releasedSeason seats were released.season, objects, ga, order_ref, events
order.completedvenuery's own checkout sold tickets (ticketed events).order, event, objects, ga, total_cents, tickets
order.refundedA venuery checkout order was refunded.order, event, refund, amount_cents, fully_refunded
event.updatedAn event changed: name, time or status (on sale, cancelled).event, key, changes, status, starts_at
chart.publishedA new chart version was published.chart, version, chart_version, capacity

Check the signature

venuery-signature holds a timestamp t and v1, the hex HMAC-SHA256 of `${t}.${raw body}` keyed with your endpoint's secret. Compute it over the raw body, before any JSON parsing, and reject old timestamps so a captured request can't be replayed.

Node (Express)
import crypto from "node:crypto";
import express from "express";

function verify(secret, rawBody, header, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds || !parts.v1) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return parts.v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}

const app = express();
app.post("/hooks/venuery", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(process.env.VENUERY_WEBHOOK_SECRET, req.body.toString("utf8"), req.get("venuery-signature") ?? "")) {
    return res.sendStatus(400);
  }
  const n = JSON.parse(req.body);
  // look up n.id to skip duplicates, then handle n.type
  res.sendStatus(200);
});
Python
import hmac, hashlib, time

def verify(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts.get("t", "0"))
    if abs(time.time() - t) > tolerance or "v1" not in parts:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Retries and ordering

  • Answer with any 2xx within 10 seconds. Anything else (including redirects, which aren't followed) is retried after 1, 2, 4, 8, 16, 32 and 64 minutes: eight tries over about two hours.
  • A notification can arrive more than once, and not always in order. Use its id to skip duplicates; when order matters, fetch the current state (/objects, the hold) rather than trusting the sequence.
  • Each endpoint's recent deliveries, with status codes and errors, are at /deliveries.
  • Missed some? Everything stays in the notifications feed: page through it with after set to the last id you saw.
Your endpoint must be a public https:// URL. Private and local addresses are refused; for local testing use a tunnel such as ngrok or Cloudflare Tunnel.