Skip to content

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.

Your PMSanother vendor's deskYour websiteyour own booking engineZimmerStackchannel manageravailability · rates · restrictionsPMSbookings · guests · folio · inventoryChannelsBooking.com · MakeMyTrip · Agoda · …

Events

TypeWhen
channel.booking.createdan OTA booking arrives through the channel manager
channel.booking.modifiedthe OTA sends a revision (dates, rooms, amounts, guest)
channel.booking.cancelledthe OTA cancels and we could apply it
zimmerstack.pingonly ever a test, from the UI or on registration

Envelope

POST to your endpoint
{
  "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.

node
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.