Sb Sabee
Docs

Webhooks: react to Sabee events in real time.

Sabee can push events to your own HTTPS endpoint the moment a reservation changes, a guest profile updates, an invoice is paid, or a channel connection breaks — so you don't have to poll the API on a schedule. This page covers the event catalogue, payload shapes, signature verification and retry behaviour.

Overview

A webhook is an HTTPS POST request Sabee sends to a URL you register, whenever a subscribed event happens on your account. Register an endpoint through POST /v1/webhooks (see the API reference) or from Settings → Developer → Webhooks in the dashboard, specifying the URL and which event types you want delivered.

Webhooks are available on Growth-tier accounts and above, the same tier that unlocks general API access. A single account can register multiple webhook endpoints — a common pattern is one endpoint per downstream system, for example one URL for your data warehouse and a separate one for a Slack notification bot, each subscribed to a different subset of events.

Event catalogue

EventFires when
reservation.createdA new reservation is created via the dashboard, a channel, the booking engine or the API.
reservation.updatedDates, room assignment, guest count or status change on an existing reservation.
reservation.cancelledA reservation is cancelled, whether by the guest, reception, or a channel-initiated cancellation.
guest.updatedA guest profile's contact details or preference notes change.
invoice.paidAn invoice moves to fully paid, whether via card, bank transfer or manual reconciliation.
channel.errorA connected OTA channel fails to sync, is throttled, or requires re-authentication.

Additional lower-volume events exist for account-level changes (room type created, rate plan updated) and are available on request for integrations that need them; the six events above cover the overwhelming majority of real integrations.

Payload examples

Every webhook delivery shares a common envelope — id, type, created_at and a data object specific to the event type.

{
  "id": "evt_7c21a0",
  "type": "reservation.created",
  "created_at": "2026-07-26T09:14:03Z",
  "data": {
    "id": "res_8a41f0",
    "status": "confirmed",
    "room_type_id": "rt_204",
    "check_in": "2026-09-14",
    "check_out": "2026-09-17",
    "channel": "direct",
    "guest_id": "gst_5521c9",
    "total": { "amount": 45000, "currency": "EUR" }
  }
}

An invoice.paid event carries the invoice object rather than a reservation:

{
  "id": "evt_7c22b4",
  "type": "invoice.paid",
  "created_at": "2026-07-26T10:02:11Z",
  "data": {
    "id": "inv_30291",
    "reservation_id": "res_8a41f0",
    "amount_paid": { "amount": 45000, "currency": "EUR" },
    "paid_at": "2026-07-26T10:02:09Z",
    "method": "card"
  }
}

A channel.error event includes the affected channel and an internal error code so your monitoring can distinguish a temporary throttle from a credential failure:

{
  "id": "evt_7c25f1",
  "type": "channel.error",
  "created_at": "2026-07-26T11:40:00Z",
  "data": {
    "channel": "booking_com",
    "code": "SB-1002",
    "message": "Channel is throttling requests; sync paused for 15 minutes."
  }
}

Verifying signatures

Every webhook delivery is signed so you can confirm it genuinely originated from Sabee and was not altered in transit. Each request carries a Sabee-Signature header containing an HMAC-SHA256 hash of the raw request body, computed with your endpoint's signing secret (shown once when you register the webhook, and re-generatable from the dashboard).

Sabee-Signature: t=1753524843,v1=5f8a1c9e2b7d4a6f1e0c9b8a7d6e5f4c3b2a1908

The header contains a timestamp (t) and the signature (v1), separated by a comma. To verify, concatenate the timestamp and the raw request body with a period, compute an HMAC-SHA256 using your signing secret, and compare the result to v1 using a constant-time comparison.

const crypto = require("crypto");

function verifySignature(rawBody, header, secret) {
  const [tPart, vPart] = header.split(",");
  const timestamp = tPart.split("=")[1];
  const signature = vPart.split("=")[1];
  const signedPayload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(signedPayload)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

Always verify against the raw request body bytes, before any JSON parsing or middleware re-serialisation — re-serializing and re-stringifying the body can change whitespace and break the signature check even when the payload is legitimate. Reject any request whose timestamp is more than five minutes old to guard against replay of a captured payload.

Retry policy

If your endpoint does not respond with a 2xx status within 10 seconds, or the connection fails outright, Sabee treats the delivery as failed and retries with exponential backoff — up to 5 attempts spread across roughly 24 hours.

AttemptDelay after previous attempt
1Immediate
2~1 minute
3~15 minutes
4~2 hours
5 (final)~20 hours

After the fifth failed attempt, the event is marked failed and no further retries occur automatically. Failed deliveries and their response codes are visible in Settings → Developer → Webhooks for up to 30 days, and can be manually replayed from that screen without waiting for a new event to trigger the same data.

IP allowlist

If your infrastructure sits behind a firewall that only accepts inbound traffic from known IP ranges, Sabee publishes a stable set of outbound webhook IP addresses that you can allowlist. The current list is available from Settings → Developer → Webhooks, and we announce any change to the range at least 30 days in advance via the changelog and email to every account with a registered webhook endpoint.

Testing webhooks

Use POST /v1/webhooks/test to trigger a synthetic event against a registered endpoint without needing to create a real reservation or wait for a real invoice payment.

curl -X POST https://api.sabee.esesun.com/v1/webhooks/test \
  -H "Authorization: Bearer $SABEE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "webhook_id": "wh_44a1", "event": "reservation.created" }'

The test event carries realistic but clearly fabricated sample data and is signed exactly like a production event, so it's a reliable way to confirm your signature verification code is correct before going live.

Ordering & idempotency

Webhook deliveries are not guaranteed to arrive in the same order events occurred, particularly during retries — a delayed reservation.updated event could in rare cases arrive after a later event for the same reservation. Design your handler to be idempotent on id (skip processing an event id you've already seen) and to treat the data object as the current state of the resource rather than assuming events arrive strictly in sequence. If strict ordering matters for your use case, use the event's created_at timestamp to reconcile state rather than relying on delivery order.

Best practices

Respond 200 OK as soon as you've durably queued the event (a database row, a message queue), rather than processing it fully inline — a slow downstream call inside your webhook handler risks the 10-second timeout and an unnecessary retry. Always verify the signature before trusting the payload, log the raw body if you need to debug a signature mismatch, and monitor your channel.error events specifically, since a channel going into a red or amber state usually needs a human to re-authenticate credentials inside the dashboard.

A few additional habits separate a webhook integration that survives real production traffic from one that quietly breaks under load. First, keep your endpoint's response time well under the 10-second timeout even during traffic spikes — if your own infrastructure is slow to accept a connection, Sabee's retry logic will treat that as a failure and schedule a retry, which can create duplicate work if your handler isn't idempotent. Second, alert on a rising rate of non-2xx responses rather than only on total failure; a webhook endpoint that fails intermittently under load often points at a downstream dependency (a database connection pool, a rate-limited third-party API) rather than the Sabee integration itself. Third, keep the signing secret out of source control and rotate it periodically from Settings → Developer → Webhooks — rotating a secret does not require re-registering the endpoint URL, only updating the value your verification code compares against.

Common integration patterns

Most Sabee webhook integrations fall into a small number of shapes. A revenue or accounting sync typically subscribes to invoice.paid and reservation.cancelled to keep an external ledger or spreadsheet in step with Sabee without polling the invoices endpoint on a timer. A front-desk or Slack notification integration typically subscribes to reservation.created and channel.error, posting a short message to a team channel so staff notice a new booking or a broken OTA connection without needing to watch the dashboard. A data-warehouse sync typically subscribes to all six event types and writes each payload into a raw events table, later transformed into whatever schema the warehouse's downstream reporting expects. Whichever pattern you're building, start from the sandbox environment described in the API reference and use the test endpoint below before pointing the integration at a live property.

Ready to register your first webhook?

Webhooks ship with Growth-tier accounts and above. Get access to set up your endpoint, or talk to us if you need help with signature verification.