Skip to main content
Everything Maat pushes to you — booking notifications and erasure requests — arrives as a signed HTTPS POST to one endpoint you register with PUT /v1/webhooks/endpoint. This page is the part of your integration that most rewards care: get verification right and every delivery you act on is provably ours; get it wrong and anyone who finds your endpoint URL can feed your system fake bookings.

Registering your endpoint

Send the URL and the events you want:
  • https only, on port 443 or 8443, on a publicly resolvable DNS name. We check the address the name actually resolves to at the moment we connect — not just at registration — and we never follow redirects. A refused URL gets one generic validation_failed; we do not report which check failed.
  • One endpoint per workspace. Registering again replaces the URL and the event list. An empty events array pauses delivery without discarding the endpoint or its secret.
  • The signing secret is returned once, in the response to your first registration, and never again — we cannot read it back. Store it in a secret manager immediately. Re-registering (a corrected path, a new event list) deliberately does not mint a new secret, so your existing verification keeps working.

The challenge round-trip

Nothing real is delivered until your endpoint proves it is yours. During the registration call we POST a signed endpoint.challenge delivery to your URL, and your handler must answer 2xx with a JSON body echoing the same string back:
Echo it and the endpoint becomes active. Anything else leaves it pending_verification and nothing is ever sent to it — call the registration endpoint again to retry. The challenge is signed exactly like a real delivery, so your verification code is exercised before your first real payload rather than after it. Respond within five seconds; the challenge happens inside your own registration request. And expect it on every registration call, including one that does not change the URL.

Verifying a delivery

Every delivery carries three headers: Four rules, in order of how often they are gotten wrong:
  1. Verify against the raw request body, before any JSON parsing. JSON.parse followed by JSON.stringify does not round-trip byte-identically — key order, whitespace and 1.01 all move — so a re-serialised body fails verification every time. Capture the bytes as they arrived.
  2. Reject a timestamp more than five minutes old before comparing anything. The timestamp is inside the signed string, so a replayed body cannot carry a fresh one.
  3. Compare in constant time, using your language’s timing-safe comparison — never == on the hex strings.
  4. During a rotation, accept either signature. X-Signature-Previous is present only while a rotation overlap is open; its absence is normal, not an error. If you lose your secret or it leaks, ask us to rotate: we sign with both secrets for an overlap window so you deploy on your own clock, never against ours.

Retries, idempotency and ordering

  • Acknowledge with any 2xx. Anything else is retried with exponential backoff; a delivery we eventually abandon is surfaced to our operators rather than silently dropped.
  • event_id is stable across retries — the same event redelivered carries the same id, so it is your idempotency key. Process each id once and answer duplicates 200.
  • Order by occurred_at, never by arrival. occurred_at is when the event happened, not when we sent it, and retries reorder deliveries.
  • We never follow redirects. If your endpoint moves, register the new URL.

What a booking notification carries — and what it never will

booking.confirmed is the arrival-management and reconciliation payload: the booking reference, the property, the stay dates with the property’s own check-in and check-out times, the party size, the guest’s name, and amount_paid as one { value, currency } object (value is a decimal string — parse it with a decimal type, not a float). Receiving that payload makes you a recipient of personal data, which carries three plain obligations:
  1. What you receive is bounded. The guest’s name and nothing else about them — no email, no phone, no durable guest identifier. Their absence is a decision, not an oversight: the guest booked on Maat and Maat carries the pre-arrival conversation. If you need to reach a guest, reach the property. Use of the data is governed by your data-sharing agreement.
  2. When the guest erases their account, you delete. See below.
  3. You never send guest data back on the calendar direction. A block carries dates and a closed reason — nothing else, enforced by the schema. See Calendar blocks.
booking.cancelled carries the same booking reference plus cancelled_at and initiated_byguest means the nights are free to re-sell, host means the property withdrew and somebody has to be re-accommodated. It repeats neither the guest’s name (you have it from the confirmation you are correlating against) nor any money: refund terms are between Maat, the guest and the property.

Erasure requests

When somebody whose booking details you received erases their Maat account, you get a user.erased delivery listing the records to delete — as booking references and your own provider ids. The payload deliberately contains no personal data at all, not even a user id: you already hold everything else from the notifications we sent, and restating a person’s details at the moment they are being destroyed would write them into your delivery log. Delete the named records from your systems, then confirm with POST /v1/deletion-requests/{event_id}/acknowledge — using the delivery’s event_id. Call it only when the deletion is actually done: there is no failure state to park a request in, and an unacknowledged request escalates on our side. Your acknowledgment never delays Maat’s own erasure, which completes regardless. The current availability of each event and endpoint is stated on its API reference page.