curl --fail-with-body -X PUT https://maatstays.wtf/v1/blocks/my-res-55123 \
-H "Authorization: Bearer $MAAT_PARTNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"listing_id": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
"check_in": "2026-09-01",
"check_out": "2026-09-05",
"reason": "reserved"
}'const res = await fetch('https://maatstays.wtf/v1/blocks/my-res-55123', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.MAAT_PARTNER_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
listing_id: '7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24',
check_in: '2026-09-01',
check_out: '2026-09-05', // exclusive — the departure day
reason: 'reserved',
}),
});
const body = await res.json();
if (res.status === 409) {
// The whole range was refused; nothing was written.
throw new Error(`nights already taken on Maat: ${body.conflicts}`);
}
if (!res.ok) throw new Error(`${body.code}: ${body.detail}`);
console.log(body.result, body.nights);
import os
import requests
res = requests.put(
"https://maatstays.wtf/v1/blocks/my-res-55123",
headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_TOKEN']}"},
json={
"listing_id": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
"check_in": "2026-09-01",
"check_out": "2026-09-05", # exclusive — the departure day
"reason": "reserved",
},
timeout=30,
)
body = res.json()
if res.status_code == 409:
# The whole range was refused; nothing was written.
raise RuntimeError(f"nights already taken on Maat: {body['conflicts']}")
if res.status_code >= 400:
raise RuntimeError(f"{body['code']}: {body.get('detail')}")
print(body["result"], body["nights"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
func main() {
payload, _ := json.Marshal(map[string]string{
"listing_id": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
"check_in": "2026-09-01",
"check_out": "2026-09-05", // exclusive — the departure day
"reason": "reserved",
})
req, _ := http.NewRequest(http.MethodPut,
"https://maatstays.wtf/v1/blocks/my-res-55123",
bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("MAAT_PARTNER_TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
var body map[string]any
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
log.Fatal(err)
}
if res.StatusCode == http.StatusConflict {
// The whole range was refused; nothing was written.
log.Fatalf("nights already taken on Maat: %v", body["conflicts"])
}
if res.StatusCode >= 400 {
log.Fatalf("%v: %v", body["code"], body["detail"])
}
fmt.Println(body["result"], body["nights"])
}
{
"external_uid": "my-res-55123",
"result": "created",
"nights": 4,
"listing_id": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24"
}Hold a range of nights with a partner block
Tells Maat that these nights are unavailable, because they are taken on your
platform. Creates the block if this external_uid is new to us and replaces its
dates otherwise.
check_out is exclusive — it is the departure day, not the last night held.
A stay of 2026-09-01 to 2026-09-05 holds four nights.
Identify the property with either your own provider_id or Maat’s listing_id,
and send exactly one of them. Both resolve. Use listing_id for a property you
did not push — we have no reference of your own for one.
Nothing about a guest belongs here. There is no field for a name, a booking
reference, an amount or a note, and sending one is a validation_failed rather
than a silent drop. Maat has no relationship with somebody who booked on your
platform and has no lawful basis to hold their details.
Pushing a block onto nights that already carry a Maat booking or a live hold
answers 409, and nothing is written: the whole range is refused rather than
the part of it that was free. conflicts[] names the colliding dates.
A listing that sells a room type over several rooms answers
409 listing_pooled_unsupported: a block closes one unit, and such a listing’s
room is chosen per booking. Nothing is written.
Re-sending the same external_uid with different dates MOVES the block — the old
nights are released and the new ones held, in one step. Re-sending it unchanged is
a no-op that answers updated.
Live in a sandbox workspace. Returns 501 against a production one, after
validating the payload in full — so keep holding these nights on your side until
the production contract is enabled.
A provider_id is resolved against the mapping your own property push
created, inside the workspace your key addresses. Two integrations using the same
reference for different properties never reach each other’s calendars, and a
reference we have never seen from you is a 404, exactly like an unknown
listing.
curl --fail-with-body -X PUT https://maatstays.wtf/v1/blocks/my-res-55123 \
-H "Authorization: Bearer $MAAT_PARTNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"listing_id": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
"check_in": "2026-09-01",
"check_out": "2026-09-05",
"reason": "reserved"
}'const res = await fetch('https://maatstays.wtf/v1/blocks/my-res-55123', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.MAAT_PARTNER_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
listing_id: '7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24',
check_in: '2026-09-01',
check_out: '2026-09-05', // exclusive — the departure day
reason: 'reserved',
}),
});
const body = await res.json();
if (res.status === 409) {
// The whole range was refused; nothing was written.
throw new Error(`nights already taken on Maat: ${body.conflicts}`);
}
if (!res.ok) throw new Error(`${body.code}: ${body.detail}`);
console.log(body.result, body.nights);
import os
import requests
res = requests.put(
"https://maatstays.wtf/v1/blocks/my-res-55123",
headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_TOKEN']}"},
json={
"listing_id": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
"check_in": "2026-09-01",
"check_out": "2026-09-05", # exclusive — the departure day
"reason": "reserved",
},
timeout=30,
)
body = res.json()
if res.status_code == 409:
# The whole range was refused; nothing was written.
raise RuntimeError(f"nights already taken on Maat: {body['conflicts']}")
if res.status_code >= 400:
raise RuntimeError(f"{body['code']}: {body.get('detail')}")
print(body["result"], body["nights"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
func main() {
payload, _ := json.Marshal(map[string]string{
"listing_id": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
"check_in": "2026-09-01",
"check_out": "2026-09-05", // exclusive — the departure day
"reason": "reserved",
})
req, _ := http.NewRequest(http.MethodPut,
"https://maatstays.wtf/v1/blocks/my-res-55123",
bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("MAAT_PARTNER_TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
var body map[string]any
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
log.Fatal(err)
}
if res.StatusCode == http.StatusConflict {
// The whole range was refused; nothing was written.
log.Fatalf("nights already taken on Maat: %v", body["conflicts"])
}
if res.StatusCode >= 400 {
log.Fatalf("%v: %v", body["code"], body["detail"])
}
fmt.Println(body["result"], body["nights"])
}
{
"external_uid": "my-res-55123",
"result": "created",
"nights": 4,
"listing_id": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24"
}Authorizations
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
Your own identifier for the block, typically your reservation id. Unique to you.
1 - 255Body
- BlockUpsertRequest
- BlockUpsertRequest
A range of nights that are unavailable. Send exactly one of provider_id or
listing_id.
No guest data of any kind is accepted here — an unrecognised field is an error, not something we quietly ignore.
Your own property identifier.
1 - 128First night held.
"2026-09-01"
Departure day, exclusive. The night before this is the last one held.
"2026-09-05"
Maat's listing identifier, for a property you did not push.
A closed set, deliberately. There is no free-text alternative, because the obvious thing to write in one is a guest's name.
See BlockReason.
reserved, maintenance, owner_stay, other Response
The nights are held.
Your own identifier for the block, echoed back.
Whether this push created the block or replaced one you had already pushed.
See WriteResult.
created, updated Nights actually held, restated so you can check it.
1 <= x <= 730The Maat listing the block landed on. Useful when you addressed the property by your own identifier and want ours in return.