Skip to main content
GET
cURL

Authorizations

Authorization
string
header
required

A short-lived partner token, obtained by exchanging your API key at POST /token. Send it as Authorization: Bearer <access_token>.

Tokens live for fifteen minutes by default and never for more than an hour. Mint one per batch of work, not one per request.

The token carries the workspace and the environment its key was issued for. You cannot change either by asking: the exchange ignores everything in the request body for exactly that reason.

It also carries the key's scopes (scope, a list). Every operation names a scope as x-maat-scope, and a token whose key lacks it is refused with 403 insufficient_scope, naming the scope. Scopes are fixed when a key is issued and are re-read from the key on every request, so a key narrowed today is narrowed on its next call — the token's scope is what the key held at exchange time, for your own reading, never what authorizes a request.

x-maat-scope is the floor, and on the quick-action operations it is not the whole requirement. PUT and DELETE /listings/{listing_id}/quick-actions/{template_key} carry rules:write, and additionally require pricing:write when template_key begins pricing. — the capability depends on the template in the path, because a cleaning fee changes what a guest pays and a minimum stay does not. The refusal is the same 403 insufficient_scope, naming pricing:write. The two READS apply the same requirement without erroring: GET /listings/{listing_id}/quick-actions still lists the pricing.* rows but withholds their applied state, and GET /listings/{listing_id}/rules omits the pricing rules entirely and reports pricing_withheld in warnings. These four are the only operations in this document whose requirement is not exactly their x-maat-scope, and each of them says so machine-readably: x-maat-scope-additional lists the second scope it can require and x-maat-scope-additional-when says what happens without it.

Never send it as a cookie or in a query string, and never store it — store the API key, mint tokens from it.

Path Parameters

delivery_id
string<uuid>
required

Our id for the delivery, as returned by GET /webhooks/deliveries.

Response

The delivery, with its body.

One delivery, with its body — the bare object. The redrive and the test event nest a delivery because each carries a second member beside it (the remaining allowance); this one has nothing to sit next to.

delivery_id
string<uuid>
required

Maat's id for this attempt record. The id redrive takes.

event_id
string<uuid>
required

The event_id in the delivered body, as a UUID. Stable across retries and a redrive.

event_type
enum<string>
required

The event this delivery carries.

See WebhookEventType.

Available options:
booking.confirmed,
booking.cancelled,
booking.requested,
booking.rejected,
booking.checked_in,
booking.checked_out,
booking.modified,
review.created,
listing.published,
listing.unlisted,
pricing.mode_changed,
pricing.currency_changed,
calendar.held,
calendar.released
status
enum<string>
required

Where the delivery is in its lifecycle. redacted means we kept the row and dropped the body. See WebhookDeliveryStatus.

Available options:
pending,
retrying,
delivering,
delivered,
failed,
redacted
attempts
integer
required

How many times we have tried to deliver it. A redrive resets this to zero.

last_error_code
string | null
required

Why the last attempt failed, from a closed vocabulary, or null if none has failed.

last_response_status
integer | null
required

The HTTP status your endpoint answered with on the last attempt, or null if it never answered.

next_attempt_at
string<date-time> | null
required

When we will try again, or null when no attempt is scheduled.

occurred_at
string<date-time>
required

The occurred_at in the delivered body. See the delivery-timestamp note on the webhook envelopes.

created_at
string<date-time>
required

When Maat queued the delivery.

payload_present
boolean
required

Whether the body is still stored. false once it has aged out or been redacted — the row survives, the body does not.

test
boolean
required

true when this delivery came from POST /webhooks/endpoint/test rather than from something that happened. The body we POSTed carries the same flag.

payload
string | null
required

The exact bytes we sent, as a JSON STRING — not a re-parsed object. null in every state but held.

payload_state
enum<string>
required

held — we still have the exact bytes; the only state a redrive can use. redacted — the delivery was still owed when the guest exercised their right to erasure, so the body was destroyed and it will never be sent; nothing is broken, there is simply nothing left to send. purged — the body aged out of the retention window, or was destroyed by an erasure after the delivery had already settled. Either way, nothing is left to send.

See WebhookPayloadState.

Available options:
held,
redacted,
purged
signature_headers
object | null
required

The signature headers the LAST attempt carried, exactly as we sent them: X-Signature, X-Signature-Timestamp, and X-Signature-Previous when a rotation overlap was open. That third key's ABSENCE is itself a fact — no second signature was sent — and not an omission.

With the byte-exact payload beside them you have the complete input to your own verifier, so a failure is diagnosable without asking anyone: a wrong secret, a wrong canonical string and a clock look different once you can compare. The values were already on the wire and are already in your own access log; this is the same disclosure to the same party.

Recomputed on read from the timestamp and the secret generations the attempt recorded — the signing secret is never stored and is never returned here. POST /webhooks/endpoint/rotate-secret is the only thing that ever returns a secret, once.

null in three honest cases: nothing was ever attempted; the body is gone (payload_state is redacted or purged), so a signature over bytes we no longer have cannot be rebuilt; or the platform cannot sign at all right now.

Example: