Skip to content

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

EventWhen
hold_createdA hold was placed
hold_cancelledAn ACTIVE hold was released before expiry
hold_expiredA hold reached hold_expiry_at without confirm
booking_createdA hold was confirmed. Not booking_confirmed.
booking_cancelledA booking was cancelled
booking_refundedA 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_id is stable across retries of the same state change.
  • delivery_id identifies this attempt.
  • timestamp in the JSON is when the event happened. Do not use it for the replay window.
  • details varies. Ignore unknown fields.
  • partner_id is 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

SuccessHTTP 2xx within 10 seconds
Retry5 attempts total
Backoff5 min, 15 min, 30 min, 60 min
Thendead_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.

Start in the sandbox. Production access is granted after certification.