Reference
Webhooks
Every OTA booking, change and cancellation that reaches ZimmerStack through the channel manager is posted, signed, to the HTTPS endpoint you register. The outbound half of the integration.
Events
| Type | When |
|---|---|
| channel.booking.created | an OTA booking arrives through the channel manager |
| channel.booking.modified | the OTA sends a revision (dates, rooms, amounts, guest) |
| channel.booking.cancelled | the OTA cancels and we could apply it |
| zimmerstack.ping | only ever a test, from the UI or on registration |
Envelope
{
"id": "evt_5d3f0c4a-6b1e-4a7e-9c1b-2f0e8d1a9b77",
"type": "channel.booking.created",
"apiVersion": "2026-09-01",
"occurredAt": "2026-09-12T08:14:03.201Z",
"mode": "live",
"property": { "id": 61, "name": "Sea Breeze Apartments", "currency": "INR", "timezone": "Asia/Kolkata" },
"data": { "booking": { "…": "the normalised booking: reference, status, stay, rooms, guest, amounts, channel" } },
"changes": null,
"previous": null
}data.booking is the normalised booking with the property's numeric room-type ids (the same ids GET /catalog gives the Connect API), never a channel-manager UUID or a credential. changes and previous are set on modified.
Signing and verification
Deliveries follow the Standard Webhooks spec: webhook-id, webhook-timestamp and webhook-signature headers, HMAC-SHA256 over id.timestamp.body with the endpoint's secret (shown once when you register it, rotatable). Verify with a constant-time compare and refuse timestamps older than five minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
// Standard Webhooks: webhook-id, webhook-timestamp, webhook-signature headers.
export function verify(headers, rawBody, secret) {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
const given = String(headers["webhook-signature"] ?? "");
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // 5-minute window
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest("base64");
return given.split(" ").some((sig) => {
const [, value] = sig.split(",");
return value && value.length === expected.length && timingSafeEqual(Buffer.from(value), Buffer.from(expected));
});
}Delivery, retries, recovery
- Your endpoint must be HTTPS on a public host (no private ranges, no redirects) and answer 2xx within 10 seconds. Anything else is retried with backoff for a day, then the endpoint is paused and the owner emailed.
- Deliveries are at-least-once and idempotent by
id: keep the last ids you processed and ignore a repeat. - The owner can redeliver one event or recover everything since an instant from the webhooks screen; your system reconciles by
data.booking.reference. - Registering, pausing, testing and rotating a secret happen in Admin → Developers → Webhooks or Channels → Integrations.
The contract
The machine-readable contract, with the full booking shape and a sample of every event, is public: https://api.zimmerstack.com/api/v1/public/webhooks/contract.