external_uid is your identifier — typically your reservation id — unique to you.
That makes every operation idempotent: re-sending a block unchanged is a no-op,
re-sending it with different dates moves it (old nights released, new nights held,
in one step), and releasing a block that is already gone is a 204 with nothing to
retry.
Dates: check_out is exclusive
check_out is the departure day, not the last night held. A block from 2026-09-01 to
2026-09-05 holds four nights, and the departure day itself stays sellable for same-day
turnover. The response restates nights so you can check the arithmetic. A single block
can hold up to 730 nights — two years, enough for any legitimate long-term closure.
No guest data. Ever. This is a rule you are bound by
Maat has no relationship with somebody who booked on your platform, and therefore no lawful basis to hold anything about them. So the block schema has no field for a guest’s name, a booking reference, an amount, or a note — and no free-text field of any kind, because the obvious thing anyone writes in one is a guest’s name. This is enforced, not requested: an unrecognised field is avalidation_failed naming
the field, never a silent drop. If your integration framework “helpfully” attaches
reservation details to outgoing payloads, you will find out on your first sandbox call
— which is exactly when you want to find out.
The only descriptive field is reason, a closed set — reserved, maintenance,
owner_stay, other — enough to tell “somebody is staying” from “the boiler is
broken” in both our audit trails, and structurally incapable of carrying more.
Conflicts: the whole range or none of it
If any night in your range already carries a Maat booking or a live hold, the push answers409 and nothing is written. We never apply the free part of a range,
because a partial apply you were told succeeded is the one failure you could not
detect — you would stop holding those nights yourself, and the same room would be sold
twice.
The problem document names every colliding date in a machine-readable conflicts[]
array (complete, unlike the human-readable sentence next to it). Act on the array:
re-push a range that excludes those dates, or leave the block off if the stay cannot be
honoured.
Note that the conflict scan runs on every push, including an identical retry. If
your nights were taken by a Maat booking after your original push, a re-push tells you
so with a 409 — which is the fact you most need, not an error to suppress.
Release frees only what the block holds
DELETE removes the block and frees the nights it is still holding. A night that is
unavailable for another reason — the property’s owner closed it, or a Maat booking has
since taken it — stays unavailable. Releasing a block is “I no longer need these nights
held”, never “declare these dates open”. There is no partial release: shorten a stay by
re-pushing the shorter range with PUT.
Reading your blocks back
GET /v1/blocks lists the blocks you pushed into the workspace your key addresses —
after a timeout or a 500, this is how you find out whether a push landed rather than
guessing. Two boundaries are fixed and are not parameters: the blocks are yours (another
partner syncing the same property never sees them, and you never see theirs), and the
workspace is the one your key was issued for. Filter by date window with from/to,
and page with page/page_size — see Pagination.
Addressing the property
Identify the property with either your ownprovider_id or Maat’s listing_id — and
send exactly one. Today, use listing_id (it is returned when you push a property,
and in every booking notification): resolving a block by provider_id is not enabled
yet and answers 501 naming the working alternative. The reference marks the current
status of each path.
Availability
Live in the sandbox — pushes, releases, reads and real409 conflicts. Against a
production workspace every request is authenticated and validated in full, then answers
501: keep holding those nights on your own platform until the production contract is
enabled.