409
conflict, a validation_failed naming the field you got wrong, a photo URL we refuse:
all of it behaves in the sandbox exactly as this documentation says.
The environment is not a parameter
Every API key is issued for exactly one workspace, and that workspace is either a sandbox or a production one. Your token carries which kind it was issued for, we compare it against the workspace on every request, and a disagreement in either direction is refused withworkspace_scope_mismatch.
There is no header, no query parameter and no request field that switches between them. To
move from sandbox to production you exchange a production key — your code otherwise does not
change.
What is isolated
Everything a sandbox workspace contains is excluded from every public surface:- sandbox properties never appear in search, on the map, or in the guest feed;
- a sandbox listing has no public listing page;
- no notification of any kind is sent for sandbox activity;
- a sandbox booking can never end in a real charge — sandbox workspaces do not reach a payment provider at all, and anything that would lead to a charge is refused with a real error rather than faked. A booking can still be MADE, with the fixture below.
Treat sandbox content as disposable
A nightly reset exists and is not switched on yet. It is scheduled for 03:30 UTC and wipes sandbox content — properties, units, listings, calendars, blocks and photo records — leaving everything your integration depends on: the workspace itself, your API key and your webhook registration. Until it is switched on, sandbox content simply accumulates; the changelog will say when that changes. Treat sandbox data as disposable either way. Build your sandbox setup as something you can re-run — the property and block contracts are idempotent by construction, so re-pushing your catalogue is one loop — rather than as something you would have to rebuild by hand.Making a booking happen
A sandbox workspace cannot reach a payment provider, so no guest can ever check out on a sandbox listing. Without something to stand in for that,GET /v1/bookings would answer an
empty page forever, confirm and reject would have nothing to act on, and the booking
webhooks — the larger half of what you build against — could not be seen at all until real
money was involved.
So there is a fixture:
POST /v1/sandbox/bookings. It creates a
booking on one of your sandbox listings the way a real one is made — the same availability
check, the same price quote, the same booking record, the same events — and delivers the
resulting booking.* webhooks to your endpoint in the same envelope with the same
signature. It is not a rehearsal of a booking. It is a booking, on inventory that only
exists in your sandbox.
"status": "pending" mints a
booking waiting on a host decision and sends booking.requested; drive it on with
POST /v1/bookings/{booking_reference}/confirm
or /reject and watch the next event arrive. "status": "confirmed" mints one already
confirmed and sends booking.confirmed straight away.
There is no guest field, and there will not be one. The guest is synthetic, minted by
us, and the same one every time for your workspace. No money moves, no payment provider is
contacted, and nobody is emailed or notified. The amount comes from your listing’s own
price for those dates, so the figures on the webhook have the shape a real booking’s do.
The booking holds real nights on your sandbox calendar, exactly as a real one would, so
a second fixture over the same dates is refused with 409 conflict — and conflicts lists
every night that is closed, the same member and the same shape PUT /v1/blocks/{external_uid} answers a collision with, so a handler you already wrote for one
works for the other. Move the window, or free the nights from your Maat console. A stay may
cover at most 30 nights.
Two refusals share 409 here, so branch on code. conflict means the nights are
taken and a different window will work. sandbox_only means you named the listing in your
own live workspace — the fixture only ever books sandbox inventory, and retrying cannot
change that. A listing that is an experience or a service answers 409 inventory_not_supported; the fixture creates stays only.
Everything else is one 404 listing_not_found: an id that names nothing, and an id that
names a listing outside your own live/sandbox pair. They are deliberately the same answer,
so the refusal cannot be used to find out what exists in another workspace.
One more thing has to be true before a delivery leaves the platform: outbound webhook delivery
is switched on per environment by a Maat operator, and it is not on by default while the
platform is pre-launch. If your endpoint is registered and verified and a fixture booking
produced no delivery, that is the reason. Developer access covers
how that is asked for today. Nothing else about your integration changes when it is switched on;
the deliveries simply start arriving.
GET /v1/webhooks/deliveries shows what we hold either
way.