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:
| Header | Value |
|---|---|
| x-quarters-event | The event name, the same as event in the body. |
| x-quarters-delivery | The delivery id, the same as id in the body. Dedupe on it. |
| x-quarters-timestamp | When the delivery was sent, in Unix seconds. |
| x-quarters-signature | sha256= 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.updatedA property was put on the website, or its changes were published.listing.deletedA property was taken off the website or deleted. Drop it from your site.calendar.updatedAvailability changed for a property: a booking, a cancellation, a block.reservation.createdA booking was created, in any status.reservation.updatedA booking changed: its status, dates, guest, money or notes.reservation.deletedA booking was deleted, usually to undo a data-entry mistake. A cancellation is an update.pingSomeone pressed the test button under Settings › Integrations.