> ## Documentation Index
> Fetch the complete documentation index at: https://developer.maatstays.wtf/llms.txt
> Use this file to discover all available pages before exploring further.

# Bookings and reviews

> What is happening on your inventory — read a booking, act on a request, reply to a review. No guest identity, anywhere.

Four operations answer "what is happening on my inventory" without ever answering "who is
staying":

| Operation | Scope | What it does |
| :- | :- | :- |
| `GET /v1/bookings` | `bookings:read` | List bookings in the workspace your key addresses. |
| `GET /v1/bookings/{booking_reference}` | `bookings:read` | Read one booking back. |
| `POST /v1/bookings/{booking_reference}/confirm` | `bookings:manage` | Accept a pending request-to-book. |
| `POST /v1/bookings/{booking_reference}/reject` | `bookings:manage` | Decline a pending request-to-book. |

When each of the seven booking webhooks fires, and in what order they can arrive, is on
[Booking notifications lifecycle](/guides/booking-lifecycle).

And two more for the reputation half of the same inventory:

| Operation | Scope | What it does |
| :- | :- | :- |
| `GET /v1/listings/{listing_id}/reviews` | `reviews:read` | Read a listing's reviews, in their public form. |
| `POST /v1/reviews/{review_id}/reply` | `reviews:reply` | Reply to a review, as the workspace. |

## 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](#reviews-reading-and-replying)
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.

```json theme={null}
{
    "booking_reference": "5f3a91c2-e8b4-4a71-9c30-1e2f3a4b5c6d",
    "property": {
        "provider_id": null,
        "listing_id": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
        "unit_id": "8e4b2a17-5c39-4d61-b0f2-7a9c3e51d804",
        "property_id": "3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7"
    },
    "inventory_kind": "unit",
    "status": "confirmed",
    "stay": {
        "check_in": "2026-10-03",
        "check_out": "2026-10-07",
        "check_in_time": "15:00",
        "check_out_time": "12:00",
        "guests": 2
    },
    "amount_paid": { "value": "5000.00", "currency": "EGP" },
    "instant_booking": true,
    "cancellation": null,
    "console_url": "https://maatstays.wtf/bookings/5f3a91c2-e8b4-4a71-9c30-1e2f3a4b5c6d",
    "created_at": "2026-09-01T10:15:00Z"
}
```

`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

```bash theme={null}
curl --fail-with-body -X POST \
  "https://maatstays.wtf/v1/bookings/5f3a91c2-e8b4-4a71-9c30-1e2f3a4b5c6d/confirm" \
  -H "Authorization: Bearer $MAAT_PARTNER_TOKEN"
```

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](/guides/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:

| Filter | Meaning |
| :- | :- |
| `status` | One of `pending`, `confirmed`, `checked_in`, `completed`, `cancelled`, `rejected`. |
| `listing_id` | Only bookings on this listing. A UUID. |
| `check_in_from` | Only bookings whose `check_in` is on or after this date (`YYYY-MM-DD`). |
| `check_in_to` | Only bookings whose `check_in` is on or before this date (`YYYY-MM-DD`). |

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.

```json theme={null}
{
    "review_id": "5e1a0c7d-…",
    "listing_id": "7c4b1e58-…",
    "reviewer_name": "Sarah M.",
    "rating": 4.5,
    "comment": "Great location, quick check-in.",
    "categories": [{ "category": "cleanliness", "rating": 5 }],
    "created_at": "2026-08-20T09:00:00Z",
    "reply": null,
    "console_url": "https://maatstays.wtf/properties/a1e0d6c5-…"
}
```

`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 `409`s, 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](/guides/sandbox).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.