Skip to main content
Four operations answer “what is happening on my inventory” without ever answering “who is staying”: When each of the seven booking webhooks fires, and in what order they can arrive, is on Booking notifications lifecycle. And two more for the reputation half of the same inventory:

There is no guest identity on a booking. Anywhere.

No name, no initial, no email, no phone number, no user reference, and no field that could be joined back to a person. The reviews read below is the one place this contract names a guest at all, and it does so in the shortened public form the listing page already shows. A booking, on this contract, is a set of facts about a stay: the dates, the door times, the party size, the status, what was paid, the listing and unit it belongs to, and whether the listing takes instant bookings.
status is one of pending, confirmed, checked_in, completed, cancelled or rejected. property is one object rather than four ids at the root, and it is the same object the booking webhooks and the publication events carry — so one accessor reads all three. inventory_kind is unit, service or experience — property.unit_id and property.property_id are null for the latter two, which have neither. guests is a count — there is no per-guest anything on this contract, and no amount of nesting would change that: the shape above is everything a booking read will ever carry. stay.check_in_time and stay.check_out_time are the unit’s own local times when inventory_kind is unit. A service or an experience hangs off no unit and has no door, so those bookings carry the platform defaults — 15:00 and 12:00 — which keeps the field always present and always a real time. Branch on inventory_kind before you staff anything from them: for a slot booking they are a placeholder, not the operator’s answer.

console_url is the answer to “who is arriving?”

Every booking, and every review, carries a deep link into the Maat host console — the same question every property-management integration asks next, answered without moving the answer onto this wire. Follow it, sign in as a person, and see what a person may see. It is slower than a field on this response, and that is what it is for: an integration that wants an arrivals board is built out of the platform’s own access control, not out of a JSON key nobody can revoke on its own.

amount_paid can be null — that is not zero

amount_paid is one object, a decimal string beside its currency, never two fields you pair yourself: { "value": "5000.00", "currency": "EGP" }. It reads null when the amount cannot be stated — a stored figure that is not a number at all — and null means “we do not know”, never “free”. The other direction matters just as much: a stay that really cost nothing reads { "value": "0.00", "currency": … }, not null. A 100% coupon, a comped night and an invitation settled at nothing are all prices, and they are the ones you most need to see in a reconciliation. A list or a read publishes null rather than a guessed 0.00, and 0.00 rather than a cautious null; a webhook, which cannot carry a null in a field meant to be spent, skips the delivery instead of sending a fabricated amount.

Confirming or rejecting a request

Both actions act only on a booking in pending. Anything else — already confirmed, already rejected, already cancelled — answers 409 conflict: nothing about your request was wrong, the booking is simply not in a state either action can change. Neither call takes a body; the response is the booking, re-read. There is no cancellation over this API. Cancelling a confirmed stay creates refunds and applies a cancellation policy — an irreversible commercial act that belongs to a person in the app, not to a DELETE a script can retry. If a stay needs to end early, that happens in the Maat host console, and the resulting booking.cancelled webhook is how you find out.

Listing your bookings

Paging is the published page / page_size / has_more rule — see Pagination. There is no total and no filter that narrows by anything about a guest, because there is nothing about a guest on this contract to filter by. GET /v1/bookings takes four filters, all optional and all about the STAY, never the guest: Send check_in_from and check_in_to together to bound a window; either one alone is an open-ended bound in that direction. Both are inclusive. If you send both and check_in_from is after check_in_to, the request is refused with 400 validation_failed rather than answered with an empty page — a window that cannot contain anything is almost always two parameters the wrong way round, and returning 200 with no items would read as “you have no bookings in May”.

Reviews: reading and replying

GET /v1/listings/{listing_id}/reviews returns the public projection — the same shortened form (“Sarah M.”, never a full name, never a user_id or booking_id) that a guest sees on the listing page. It is public because the content already is; the developer contract reads it rather than duplicating it.
console_url is the property’s page in the host console — the reputation surface a person acts on, since there is no per-review page. It reads null for a review on a service or an experience listing, which hangs off no property at all. POST /v1/reviews/{review_id}/reply posts one field, { "body": "…" }, and publishes it exactly as the host app does: authored by the workspace, never by a named person, because letting a caller attach an author would put somebody’s name on public content through an API key. The response is the review itself, re-read with the reply attached — the bare object, not a wrapper.

When we cannot answer

All six operations here — the two reads, the two decisions, the review list and the reply — read and write through Maat’s own booking service. When that does not answer, they say so: 503 service_unavailable, with a detail naming what did not happen. Nothing was read, nothing was disclosed, nothing was changed, and retrying is the right thing to do. Handle it as its own branch. The alternative would be a fabricated answer to a question you asked in good faith — an empty page that reads as “you have no bookings”, or a 404 booking_not_found for a stay that is sitting there pending — and either one is worse than being told we could not answer. It is a different code from 500 internal_error, which is a fault of ours that a retry will hit again.

Current status

Both surfaces are live in a sandbox workspace — real reads, real 409s, a real reply. Against a production workspace every request is authenticated and validated in full, then answers 501: nothing is read, nothing is written, and no guest is disclosed. A 501 means your integration is correct — see Sandbox.