Skip to content
Developers
esc
  • Type an endpoint, an object or a word from a guide.

Webhooks

Webhooks

Register an endpoint, verify the signature, read the record.

Register an https endpoint and Quarters POSTs a signed delivery to it when something changes.

Register an endpoint

Under Settings › Integrations, add the URL. Its secret (whsec_…) is shown next to it. The test button sends a ping the way a real event goes, and recent deliveries are listed under the endpoint with their outcome.

The URL must be https and resolve to a public address. That is checked on every delivery, not on save, so a hostname that later points inward is refused then.

What arrives

The body is an envelope: the delivery id, the event, createdAt, and a small data object that points at the record. Read the current state from the API, and a late or repeated delivery does no harm.

Delivery body

{
  "id": "6f1c…",
  "event": "reservation.updated",
  "createdAt": "2026-09-13T10:00:00.000Z",
  "data": {
    "reservationId": "7c02…",
    "propertyId": "0d6e1c2a-…",
    "status": "confirmed",
    "previousStatus": "reserved"
  }
}

Four headers travel with it:

HeaderValue
x-quarters-eventThe event name, the same as event in the body.
x-quarters-deliveryThe delivery id, the same as id in the body. Dedupe on it.
x-quarters-timestampWhen the delivery was sent, in Unix seconds.
x-quarters-signaturesha256= followed by the hex HMAC-SHA256, under your endpoint’s secret, of the timestamp, a dot and the raw body.

Each delivery also carries x-housalot-event, x-housalot-delivery and x-housalot-signature, the headers’ former names. That signature covers the body alone, so a captured delivery would verify forever. Verify x-quarters-signature. The old headers go away with notice.

Verify the signature

Compute HMAC-SHA256 under the endpoint’s secret over x-quarters-timestamp, a dot, and the raw body: the bytes as received, before any JSON parsing. Compare it in constant time with x-quarters-signature. Refuse a timestamp more than five minutes from your own clock, so a captured delivery stops verifying. Reject anything that fails before you parse the body.

Node

import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 5 * 60;

// rawBody is the request body exactly as received, before any JSON parsing.
export function verify(secret, rawBody, headers) {
  const timestamp = headers["x-quarters-timestamp"] ?? "";
  if (!/^\d+$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;
  const expected = `sha256=${createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest("hex")}`;
  const actual = headers["x-quarters-signature"] ?? "";
  return actual.length === expected.length &&
    timingSafeEqual(Buffer.from(actual), Buffer.from(expected));
}

Python

import hashlib
import hmac
import time

TOLERANCE_SECONDS = 5 * 60

# raw_body is the request body exactly as received, before any JSON parsing.
def verify(secret: str, raw_body: bytes, headers) -> bool:
    timestamp = headers.get("x-quarters-timestamp") or ""
    if not timestamp.isdigit():
        return False
    if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        return False
    signed = timestamp.encode() + b"." + raw_body
    expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(headers.get("x-quarters-signature") or "", expected)

Handle it

  • Answer fast. Any 2xx counts as received. Queue the work and return. After ten seconds the delivery counts as failed.
  • Dedupe on the delivery id. A delivery is sent once, but your side may see it twice through a retrying proxy. The id is in the body and in the header.
  • Read, don’t trust. The envelope says what changed; the API says what it is now. Fetch the record and act on that.
  • Plan for a miss. Each delivery is one attempt, with no retry queue. Recover with a full read: listings are one call, reservations page with updatedSince.

The events

7 events, listed in full with their payloads under Webhook events.

  • listing.updated A property was put on the website, or its changes were published.
  • listing.deleted A property was taken off the website or deleted. Drop it from your site.
  • calendar.updated Availability changed for a property: a booking, a cancellation, a block.
  • reservation.created A booking was created, in any status.
  • reservation.updated A booking changed: its status, dates, guest, money or notes.
  • reservation.deleted A booking was deleted, usually to undo a data-entry mistake. A cancellation is an update.
  • ping Someone pressed the test button under Settings › Integrations.