curl --fail-with-body -X PUT https://maatstays.wtf/v1/properties/3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7/address \
-H "Authorization: Bearer $MAAT_PARTNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"line1": "14 Example Street",
"city": "Alexandria",
"region": "Alexandria",
"postal_code": "21500",
"country": "EG",
"latitude": 31.2001,
"longitude": 29.9187,
"building": "Nile Court",
"floor": "G",
"apartment": "3A",
"directions": "Blue door beside the pharmacy."
}'// Every member is written every time — one you omit is cleared.
const address = {
line1: '14 Example Street',
city: 'Alexandria',
region: 'Alexandria',
postal_code: '21500',
country: 'EG',
latitude: 31.2001,
longitude: 29.9187,
building: 'Nile Court',
floor: 'G',
apartment: '3A',
directions: 'Blue door beside the pharmacy.',
};
const res = await fetch(
'https://maatstays.wtf/v1/properties/3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7/address',
{
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.MAAT_PARTNER_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(address),
},
);
const body = await res.json();
if (!res.ok) throw new Error(`${body.code}: ${body.detail}`);
// The time zone and the area come back re-derived from the coordinates.
console.log(body.address.city, body.timezone);
import os
import requests
# Every member is written every time — one you omit is cleared.
address = {
"line1": "14 Example Street",
"city": "Alexandria",
"region": "Alexandria",
"postal_code": "21500",
"country": "EG",
"latitude": 31.2001,
"longitude": 29.9187,
"building": "Nile Court",
"floor": "G",
"apartment": "3A",
"directions": "Blue door beside the pharmacy.",
}
res = requests.put(
"https://maatstays.wtf/v1/properties/3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7/address",
headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_TOKEN']}"},
json=address,
timeout=30,
)
body = res.json()
if res.status_code >= 400:
raise RuntimeError(f"{body['code']}: {body.get('detail')}")
print(body["address"]["city"], body["timezone"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
type property struct {
Timezone string `json:"timezone"`
}
func main() {
// Every member is written every time — one you omit is cleared.
payload, _ := json.Marshal(map[string]any{
"line1": "14 Example Street",
"city": "Alexandria",
"region": "Alexandria",
"postal_code": "21500",
"country": "EG",
"latitude": 31.2001,
"longitude": 29.9187,
"building": "Nile Court",
"floor": "G",
"apartment": "3A",
"directions": "Blue door beside the pharmacy.",
})
req, _ := http.NewRequest(http.MethodPut,
"https://maatstays.wtf/v1/properties/3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7/address",
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()
if res.StatusCode >= 400 {
log.Fatalf("address write failed: %d", res.StatusCode)
}
var body property
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
log.Fatal(err)
}
fmt.Println(body.Timezone)
}
{
"property_id": "3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7",
"provider_id": "my-prop-9001",
"property_type": "apartment",
"address": {
"line1": "14 Example Street",
"city": "Alexandria",
"region": "Alexandria",
"postal_code": "21500",
"country": "EG",
"latitude": 31.2001,
"longitude": 29.9187,
"building": "3",
"floor": "2",
"apartment": "12",
"directions": "Blue door on the courtyard side; the bell is on the left."
},
"timezone": "Africa/Cairo",
"amenities": [
"wifi",
"air-conditioning",
"washing-machine"
],
"units": [
{
"unit_id": "8e4b2a17-5c39-4d61-b0f2-7a9c3e51d804",
"unit_type": "apartment",
"max_guests": 4,
"bedrooms": 2,
"beds": 3,
"bathrooms": 1,
"check_in_time": "15:00",
"check_out_time": "12:00",
"amenities": [
"wifi",
"air-conditioning",
"washing-machine"
],
"house_rules": [
{
"slug": "pets_allowed",
"allowed": true
},
{
"slug": "smoking_allowed",
"allowed": false
}
],
"listings": [
{
"listing_id": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
"inventory_kind": "unit",
"title": "Courtyard flat with the blue door",
"description": "A two-bedroom flat one street back from the corniche.",
"status": "listed",
"base_rate": {
"value": "1250.00",
"currency": "EGP"
},
"cleaning_fee": {
"value": "200.00",
"currency": "EGP"
},
"instant_booking": true,
"is_exclusive": false,
"is_best_price": true,
"rating": {
"average": 4.8,
"count": 24
},
"publishable": true,
"missing": [],
"console_url": "https://maatstays.wtf/properties/3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7"
}
]
}
],
"publishable": true,
"missing": [],
"created_at": "2026-08-30T18:42:11Z",
"updated_at": "2026-09-02T09:15:40Z",
"console_url": "https://maatstays.wtf/properties/3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7"
}Replace a property's address
Replaces the whole address of a property.
Replace, not merge. Every member is written on every call — as the value you
sent, or as null. Omitting one CLEARS it, and that is deliberate: a stale floor
number left under a new street sends a guest to the right building and the wrong
door.
The four private delivery fields — building, floor, apartment,
directions — are written here, and are absent from the property push. They
tell a guest which bell to ring once a stay is confirmed, and they are replaced
on every call like everything else in the body.
Three things are re-derived from the coordinates in the body and are not yours to send: the map position, the platform’s area assignment and the property’s time zone. A coordinate pair that falls outside every curated area is not an error — the area is simply left unset.
Text is normalised before it is stored, and its length is re-checked afterwards: control characters, zero-width characters and bidirectional-override characters are removed, and runs of whitespace are collapsed. A value that normalises to nothing is refused rather than stored as an empty string.
The body is strict. An undeclared field is a 400 validation_failed naming it,
never a silent drop, and a stray query string is refused the same way.
A property with more than one unit, or more than one listing, is refused with
409 property_not_simple; a listing that is not a stay with
409 inventory_not_supported. Nothing is written either way.
Live in a sandbox workspace. Returns 501 against a production one.
curl --fail-with-body -X PUT https://maatstays.wtf/v1/properties/3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7/address \
-H "Authorization: Bearer $MAAT_PARTNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"line1": "14 Example Street",
"city": "Alexandria",
"region": "Alexandria",
"postal_code": "21500",
"country": "EG",
"latitude": 31.2001,
"longitude": 29.9187,
"building": "Nile Court",
"floor": "G",
"apartment": "3A",
"directions": "Blue door beside the pharmacy."
}'// Every member is written every time — one you omit is cleared.
const address = {
line1: '14 Example Street',
city: 'Alexandria',
region: 'Alexandria',
postal_code: '21500',
country: 'EG',
latitude: 31.2001,
longitude: 29.9187,
building: 'Nile Court',
floor: 'G',
apartment: '3A',
directions: 'Blue door beside the pharmacy.',
};
const res = await fetch(
'https://maatstays.wtf/v1/properties/3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7/address',
{
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.MAAT_PARTNER_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(address),
},
);
const body = await res.json();
if (!res.ok) throw new Error(`${body.code}: ${body.detail}`);
// The time zone and the area come back re-derived from the coordinates.
console.log(body.address.city, body.timezone);
import os
import requests
# Every member is written every time — one you omit is cleared.
address = {
"line1": "14 Example Street",
"city": "Alexandria",
"region": "Alexandria",
"postal_code": "21500",
"country": "EG",
"latitude": 31.2001,
"longitude": 29.9187,
"building": "Nile Court",
"floor": "G",
"apartment": "3A",
"directions": "Blue door beside the pharmacy.",
}
res = requests.put(
"https://maatstays.wtf/v1/properties/3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7/address",
headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_TOKEN']}"},
json=address,
timeout=30,
)
body = res.json()
if res.status_code >= 400:
raise RuntimeError(f"{body['code']}: {body.get('detail')}")
print(body["address"]["city"], body["timezone"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
type property struct {
Timezone string `json:"timezone"`
}
func main() {
// Every member is written every time — one you omit is cleared.
payload, _ := json.Marshal(map[string]any{
"line1": "14 Example Street",
"city": "Alexandria",
"region": "Alexandria",
"postal_code": "21500",
"country": "EG",
"latitude": 31.2001,
"longitude": 29.9187,
"building": "Nile Court",
"floor": "G",
"apartment": "3A",
"directions": "Blue door beside the pharmacy.",
})
req, _ := http.NewRequest(http.MethodPut,
"https://maatstays.wtf/v1/properties/3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7/address",
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()
if res.StatusCode >= 400 {
log.Fatalf("address write failed: %d", res.StatusCode)
}
var body property
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
log.Fatal(err)
}
fmt.Println(body.Timezone)
}
{
"property_id": "3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7",
"provider_id": "my-prop-9001",
"property_type": "apartment",
"address": {
"line1": "14 Example Street",
"city": "Alexandria",
"region": "Alexandria",
"postal_code": "21500",
"country": "EG",
"latitude": 31.2001,
"longitude": 29.9187,
"building": "3",
"floor": "2",
"apartment": "12",
"directions": "Blue door on the courtyard side; the bell is on the left."
},
"timezone": "Africa/Cairo",
"amenities": [
"wifi",
"air-conditioning",
"washing-machine"
],
"units": [
{
"unit_id": "8e4b2a17-5c39-4d61-b0f2-7a9c3e51d804",
"unit_type": "apartment",
"max_guests": 4,
"bedrooms": 2,
"beds": 3,
"bathrooms": 1,
"check_in_time": "15:00",
"check_out_time": "12:00",
"amenities": [
"wifi",
"air-conditioning",
"washing-machine"
],
"house_rules": [
{
"slug": "pets_allowed",
"allowed": true
},
{
"slug": "smoking_allowed",
"allowed": false
}
],
"listings": [
{
"listing_id": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
"inventory_kind": "unit",
"title": "Courtyard flat with the blue door",
"description": "A two-bedroom flat one street back from the corniche.",
"status": "listed",
"base_rate": {
"value": "1250.00",
"currency": "EGP"
},
"cleaning_fee": {
"value": "200.00",
"currency": "EGP"
},
"instant_booking": true,
"is_exclusive": false,
"is_best_price": true,
"rating": {
"average": 4.8,
"count": 24
},
"publishable": true,
"missing": [],
"console_url": "https://maatstays.wtf/properties/3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7"
}
]
}
],
"publishable": true,
"missing": [],
"created_at": "2026-08-30T18:42:11Z",
"updated_at": "2026-09-02T09:15:40Z",
"console_url": "https://maatstays.wtf/properties/3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7"
}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
Maat's identifier for the property, as returned by GET /properties. Not your
own provider_id — the write endpoint is the one keyed on that, which is why
this read carries a by-id segment.
Body
A property's whole address, replaced.
Every member is written on every call — as the value you sent, or as null. One
you omit is CLEARED, not kept.
The last four are private delivery detail — which building, which floor, which door — and they are accepted on this contract only. They are not part of the property push.
Street line.
1 - 200City or town.
1 - 120ISO 3166-1 alpha-2, uppercase.
^[A-Z]{2}$The map position, the platform's area assignment and the property's time zone are all re-derived from this pair — do not send them. A pair that falls outside every curated area leaves the area unset, which is not an error.
-90 <= x <= 90Decimal degrees, WGS 84.
-180 <= x <= 180State, province or governorate. Absent or null clears it.
120Absent or null clears it.
20Private delivery detail. Absent or null clears it.
200Private delivery detail, and a STRING rather than a number — real answers
include "G", "Mezzanine" and "3A". Absent or null clears it.
40Private delivery detail. Absent or null clears it.
40Private delivery detail — how to find the door once you are at the
building. Absent or null clears it.
400Response
The property, read back after the change.
One property, with its units and their listings.
console_url is a deep link into the Maat host console.
Maat's id for the property. The id the targeted edits take — never your provider_id.
Your identifier for the property, when you pushed it. null when you
did not.
The property-type slug, from GET /catalog/property-types.
Where the property is.
The last four members are private delivery detail a host typed for the guest who has to find the door. Keep them out of anything your system publishes.
Show child attributes
Show child attributes
IANA zone.
"Africa/Cairo"
Building-level amenity slugs: every slug offered by at least one of this
property's units, de-duplicated. For a property that is one unit — which
is every property the write operations accept — it is exactly that unit's
set, so PUT /v1/properties/{property_id}/amenities and this field agree.
The per-unit sets are still on each unit. A hotel's own facilities
(property-scoped amenities) are not on this contract yet.
The physical inventory under the property. One today for anything this API can create.
Show child attributes
Show child attributes
True when every listing under this property satisfies the gate.
The de-duplicated union of its listings' reasons.
See MissingReason.
Why a listing is not publishable. A closed vocabulary.
price, amenities, cover_photo, photos, photos_processing, unlisted When the property was created in Maat.
When the property was last changed, by anyone — this API, the host app, or Maat.
The property's page in the Maat host console.