Appearance
Webhooks
Subscribe to HTTP callbacks when booking state changes — including changes you did not initiate (walk-ins, other partners). That is how you keep a local availability cache fresh.
Auth for manage-subscription calls: x-api-key. The subscription is always yours; partner_id in the body is ignored.
Subscribe
http
POST /api/v1/partner/webhooks
Content-Type: application/json
x-api-key: <your-api-key>json
{
"endpoint_url": "https://partner.example.com/cbms/webhooks",
"event_types": [
"booking_created",
"booking_cancelled",
"hold_expired",
"hold_cancelled"
]
}201 includes secret_key. Store it. You need it to verify signatures. It is returned on create and on rotate (POST /api/v1/partner/webhooks/{id}/rotate-secret). Do not rely on listing subscriptions to retrieve it.
GET /api/v1/partner/webhooks lists your subscriptions.
There is no partner-facing unsubscribe or deactivate today. To stop deliveries, contact the platform team (or rotate the secret and reject unsigned traffic). That gap is on us, not something you should invent a DELETE for.
Event types
| Event | When |
|---|---|
hold_created | A hold was placed |
hold_cancelled | An ACTIVE hold was released before expiry |
hold_expired | A hold reached hold_expiry_at without confirm |
booking_created | A hold was confirmed. Not booking_confirmed. |
booking_cancelled | A booking was cancelled |
booking_refunded | A booking was refunded |
You receive events for subscribed types on venues you care about, including other channels. Fan-out is by event type.
Delivery body
json
{
"event_id": "…",
"event_type": "booking_created",
"timestamp": "2026-09-20T17:12:43.812Z",
"venue_id": "…",
"slot_id": "…",
"hold_id": "…",
"booking_id": "…",
"partner_id": "…",
"details": {},
"delivery_id": "…"
}event_idis stable across retries of the same state change.delivery_ididentifies this attempt.timestampin the JSON is when the event happened. Do not use it for the replay window.detailsvaries. Ignore unknown fields.partner_idis the originator of the change, which may not be you.
Headers
http
Content-Type: application/json
X-Webhook-Event: booking_created
X-Delivery-Id: <same as delivery_id>
X-Delivery-Attempt: 1
X-CBMS-Timestamp: 1726850000
X-CBMS-Signature: sha256=<hex>X-CBMS-Timestamp is Unix seconds when this attempt was sent. Retries get a fresh timestamp and signature. A retry 45 minutes later would fail a 5-minute window if we signed the original event time.
Verify (Node)
HMAC-SHA256 over `${timestamp}.${rawBody}` using secret_key. Compare in constant time. Reject if |now - timestamp| > 300 seconds.
javascript
const crypto = require('crypto');
function verify(secretKey, timestampHeader, rawBody, signatureHeader) {
const timestamp = Number(timestampHeader);
if (!Number.isFinite(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const match = /^sha256=([0-9a-f]+)$/i.exec((signatureHeader || '').trim());
if (!match) return false;
const expected = crypto
.createHmac('sha256', secretKey)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(match[1].toLowerCase(), 'utf8');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}Use the raw body bytes as received. Re-serializing JSON will break the HMAC.
Retries and dead-letter
| Success | HTTP 2xx within 10 seconds |
| Retry | 5 attempts total |
| Backoff | 5 min, 15 min, 30 min, 60 min |
| Then | dead_letter — we stop. We are alerted. You can inspect /api/v1/partner/observability. |
Your handler should:
- Return 2xx quickly; queue downstream work.
- Be idempotent on
event_id(the same event can arrive twice if our 2xx was lost). - Tolerate out-of-order delivery.