{
    "info": {
        "_postman_id": "9f3c2a10-7b45-4c8e-9d61-2f0a5b7c4e13",
        "name": "Maat Stays Partner API",
        "description": "Manage your Maat listings from your own system.\n\nGenerated from the published OpenAPI document (`info.version`: 2026-10-16).\nDo not edit by hand — run `pnpm openapi:postman`.\n\nSet `apiKey` to your `maat_partner_…` key, run **Exchange your API key for a token**\nonce, and every other request is authorized from the `accessToken` it stores.",
        "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
    },
    "auth": {
        "type": "bearer",
        "bearer": [
            {
                "key": "token",
                "value": "{{accessToken}}",
                "type": "string"
            }
        ]
    },
    "event": [
        {
            "listen": "test",
            "script": {
                "type": "text/javascript",
                "exec": [
                    "// Stores the token the exchange returns, so every other request is authorized.",
                    "if (pm.request.url.getPath().endsWith(\"/token\") && pm.response.code === 200) {",
                    "    pm.collectionVariables.set(\"accessToken\", pm.response.json().access_token);",
                    "}"
                ]
            }
        }
    ],
    "variable": [
        {
            "key": "baseUrl",
            "value": "https://maatstays.wtf/v1",
            "type": "string"
        },
        {
            "key": "apiKey",
            "value": "",
            "type": "string"
        },
        {
            "key": "accessToken",
            "value": "",
            "type": "string"
        }
    ],
    "item": [
        {
            "name": "authentication",
            "description": "Exchanging your API key for the short-lived token every other call carries.\n",
            "item": [
                {
                    "name": "Exchange your API key for a token",
                    "request": {
                        "method": "POST",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/token",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "token"
                            ]
                        },
                        "description": "Swap your long-lived API key (`maat_partner_…`) for a short-lived bearer token\nscoped to exactly one workspace and one environment. Send the token on every\nother call in this document.\n\nTokens live for fifteen minutes by default and never for more than an hour. Mint\none per batch of work, not one per request, and re-exchange on any `401`.\n\n**The request body is empty, and that is the control rather than a formality.**\nThe workspace, the environment and the scopes on the token all come from the key\nyou presented. The endpoint declares no fields at all, and anything you send is\ndeleted before the handler runs — a request carrying a `workspace_id` or a\n`sandbox` flag mints exactly the token an empty request would. There is nothing\nyou could send that would change what the token can reach.\n\n**Works against both kinds of workspace** — sandbox and production alike. It is\nhow you authenticate, so it answers before anything else does, and it is the one\noperation in this document that never returns `501`. Read `sandbox` in the\nresponse to find out which kind of workspace your key was issued for; it is the\nonly place that answer is published.\n\n\nThe test script below stores `access_token` in the `accessToken` collection variable,\nwhich every other request in this collection sends. Run this one first.",
                        "auth": {
                            "type": "bearer",
                            "bearer": [
                                {
                                    "key": "token",
                                    "value": "{{apiKey}}",
                                    "type": "string"
                                }
                            ]
                        }
                    },
                    "response": []
                }
            ]
        },
        {
            "name": "properties",
            "description": "Push your properties into Maat from your own system, keyed by that system's own\n`provider_id`, and read back everything your workspace has on Maat.\n",
            "item": [
                {
                    "name": "Create or replace a property",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/properties/:provider_id",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "properties",
                                ":provider_id"
                            ],
                            "variable": [
                                {
                                    "key": "provider_id",
                                    "value": "my-prop-9001",
                                    "description": "Your own identifier for the property. Unique to your workspace — it never\ncollides with another workspace's.\n"
                                }
                            ]
                        },
                        "description": "Creates the property if we have not seen this `provider_id` from you before, and\nreplaces it otherwise. Your `provider_id` is yours: it is unique within your\nintegration and never across the platform.\n\n**Replace, not merge.** The payload you send is the whole property. An amenity you\nomit is removed; a photo you omit is dropped. This mirrors how listings are edited\neverywhere else on Maat, and it is the only version of \"update\" in which\n\"remove this amenity\" is expressible.\n\nA property that cannot yet be sold — no price, no photos, no amenities — is\naccepted and held in an incomplete state rather than published at a placeholder\nprice. `publishable` and `missing` tell you exactly what it is waiting for, and\n`listing_id` stays `null` until it can actually be shown to a guest.\n\n`property_id` and `unit_id` come back on **every** push, publishable or not, and\nthey are the handles the targeted edits take — the address, the amenity set, the\nunit's occupancy and its house rules. Store them beside your own `provider_id`\nand you never have to look one up.\n\n**Photos are fetched asynchronously**, so the FIRST push of a complete property\ncomes back `publishable: false` with `photos_processing`. That is not a failure\nand there is nothing to do: we fetch each URL, process it and publish the\nproperty on its own. Anything we could not turn into a photo is named in\n`photos_rejected`. Sending the same URL again is free — an unchanged URL is never\nre-fetched, so a nightly catalogue sync costs nothing for the photos that did not\nchange. Change the bytes behind a URL and publish it at a new URL.\n\nSend only the address fields listed here. Delivery detail (building, floor,\napartment number, directions) is not part of this contract in either direction.\n\n**Live in a sandbox workspace. Returns `501` against a production one**, where\nthe payload is authenticated and validated in full and no property is written. A\nsandbox token creates real properties, excluded from every public surface —\nsearch, the map, the guest feed.\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"name\": \"Courtyard flat with the blue door\",\n  \"description\": \"A two-bedroom flat one street back from the corniche.\",\n  \"property_type\": \"apartment\",\n  \"address\": {\n    \"line1\": \"14 Example Street\",\n    \"city\": \"Alexandria\",\n    \"region\": \"Alexandria\",\n    \"postal_code\": \"21500\",\n    \"country\": \"EG\",\n    \"latitude\": 31.2001,\n    \"longitude\": 29.9187\n  },\n  \"occupancy\": {\n    \"max_guests\": 4,\n    \"bedrooms\": 2,\n    \"beds\": 3,\n    \"bathrooms\": 1\n  },\n  \"base_rate\": {\n    \"value\": \"1250.00\",\n    \"currency\": \"EGP\"\n  },\n  \"amenities\": [\n    \"wifi\",\n    \"air-conditioning\",\n    \"washing-machine\"\n  ],\n  \"photos\": [\n    {\n      \"url\": \"https://images.example-partner.invalid/p/9001/1.jpg\",\n      \"position\": 0\n    },\n    {\n      \"url\": \"https://images.example-partner.invalid/p/9001/2.jpg\",\n      \"position\": 1\n    }\n  ],\n  \"check_in_time\": \"15:00\",\n  \"check_out_time\": \"12:00\",\n  \"status\": \"listed\"\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                },
                {
                    "name": "List your properties",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/properties?page=&page_size=",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "properties"
                            ],
                            "query": [
                                {
                                    "key": "page",
                                    "value": "1",
                                    "description": "1-based page number. Defaults to the first page.",
                                    "disabled": true
                                },
                                {
                                    "key": "page_size",
                                    "value": "25",
                                    "description": "Rows per page. The same default and the same ceiling on every list in this API.\n\nA `page_size` above the maximum is **refused** with `validation_failed` naming\nthe parameter — never silently clamped, because a caller who believes they asked\nfor 500 and received 200 will stop walking a set they have not finished.\n",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "One page of your workspace's properties, newest first.\n\nThat is the whole workspace, including properties created in the Maat app that\nhave no `provider_id` at all.\n\nEach entry carries its units and their listings, so a catalogue sync is one walk\nrather than one call per property. `publishable` and `missing` say whether each\none satisfies the platform's validity gate — a price above zero, at least one\namenity, a cover photo and a minimum photo count.\n\nPaged with `page` and `page_size`; keep asking while `has_more` is `true`. There\nis no total.\n\n**Live in a sandbox workspace. Returns `501` against a production one**, where\nnothing is read and nothing is disclosed.\n"
                    },
                    "response": []
                },
                {
                    "name": "Read one property",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/properties/by-id/:property_id",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "properties",
                                "by-id",
                                ":property_id"
                            ],
                            "variable": [
                                {
                                    "key": "property_id",
                                    "value": "3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7",
                                    "description": "Maat's identifier for the property, as returned by `GET /properties`. Not your\nown `provider_id` — the write endpoint is the one keyed on that, which is why\nthis read carries a `by-id` segment.\n"
                                }
                            ]
                        },
                        "description": "One property, with its units and their listings.\n\nAddressed by **Maat's** `property_id`, not by your own `provider_id`. Those are\ndifferent questions and both are useful: `PUT /properties/{provider_id}` writes\nby your reference, and this reads by ours — which is the only handle a property\ncreated in the Maat app has.\n\nThe `by-id` segment is what keeps those two apart. `/properties/{provider_id}`\nand `/properties/{property_id}` are the SAME URL template as far as OpenAPI is\nconcerned, so one of them has to say which identifier it means. The write was\npublished first and is not moving; the read says so in its path.\n\n`provider_id` in the response is **yours**. A property you did not push comes back\nwith `null`.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Replace a property's address",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/properties/:property_id/address",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "properties",
                                ":property_id",
                                "address"
                            ],
                            "variable": [
                                {
                                    "key": "property_id",
                                    "value": "3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7",
                                    "description": "Maat's identifier for the property, as returned by `GET /properties`. Not your\nown `provider_id` — the write endpoint is the one keyed on that, which is why\nthis read carries a `by-id` segment.\n"
                                }
                            ]
                        },
                        "description": "Replaces the whole address of a property.\n\n**Replace, not merge.** Every member is written on every call — as the value you\nsent, or as `null`. Omitting one CLEARS it, and that is deliberate: a stale floor\nnumber left under a new street sends a guest to the right building and the wrong\ndoor.\n\nThe four private delivery fields — `building`, `floor`, `apartment`,\n`directions` — are written **here**, and are absent from the property push. They\ntell a guest which bell to ring once a stay is confirmed, and they are replaced\non every call like everything else in the body.\n\nThree things are re-derived from the coordinates in the body and are not yours to\nsend: the map position, the platform's area assignment and the property's time\nzone. A coordinate pair that falls outside every curated area is **not an error**\n— the area is simply left unset.\n\nText is normalised before it is stored, and its length is re-checked afterwards:\ncontrol characters, zero-width characters and bidirectional-override characters\nare removed, and runs of whitespace are collapsed. A value that normalises to\nnothing is refused rather than stored as an empty string.\n\nThe body is strict. An undeclared field is a `400 validation_failed` naming it,\nnever a silent drop, and a stray query string is refused the same way.\n\nA property with more than one unit, or more than one listing, is refused with\n`409 property_not_simple`; a listing that is not a stay with\n`409 inventory_not_supported`. Nothing is written either way.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"line1\": \"14 Example Street\",\n  \"city\": \"Alexandria\",\n  \"region\": \"Alexandria\",\n  \"postal_code\": \"21500\",\n  \"country\": \"EG\",\n  \"latitude\": 31.2001,\n  \"longitude\": 29.9187,\n  \"building\": \"Nile Court\",\n  \"floor\": \"G\",\n  \"apartment\": \"3A\",\n  \"directions\": \"Blue door beside the pharmacy.\"\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                },
                {
                    "name": "Replace a property's amenities",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/properties/:property_id/amenities",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "properties",
                                ":property_id",
                                "amenities"
                            ],
                            "variable": [
                                {
                                    "key": "property_id",
                                    "value": "3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7",
                                    "description": "Maat's identifier for the property, as returned by `GET /properties`. Not your\nown `provider_id` — the write endpoint is the one keyed on that, which is why\nthis read carries a `by-id` segment.\n"
                                }
                            ]
                        },
                        "description": "Replaces the amenity set of the property's single unit — the set Maat's own\nvalidity gate counts, and the same set a whole-property push replaces.\n\n**Replace, not merge.** A slug you omit is removed. Send at least one and at most\nsixty, with no repeats. The vocabulary is `GET /catalog/amenities`; map yours onto\nit once at the start of a sync rather than discovering it one property at a\ntime.\n\n**An unknown slug is echoed, not refused.** `amenities` in the response is the set\nnow stored — only what resolved — and `unknown_amenities` is the rest. One\nunmapped slug does not block an otherwise good property while you finish your\nmapping.\n\n**If none of them resolved, the whole call is a `400` and nothing is written.**\nApplying that result would clear the set, and a property with no amenity fails\nthe validity gate — so an unmapped vocabulary would take a live listing off sale\nas a side effect of an edit nobody meant that way.\n\nThe body is strict. An undeclared field is a `400 validation_failed` naming it,\nnever a silent drop, and a stray query string is refused the same way.\n\nA property with more than one unit, or more than one listing, is refused with\n`409 property_not_simple`; a listing that is not a stay with\n`409 inventory_not_supported`. Nothing is written either way.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"amenities\": [\n    \"wifi\",\n    \"air-conditioning\",\n    \"washing-machine\"\n  ]\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                }
            ]
        },
        {
            "name": "units",
            "description": "The physical inventory under a property: how many it sleeps, its door times, its\nhouse-rule answers.\n",
            "item": [
                {
                    "name": "Read one unit",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/units/:unit_id",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "units",
                                ":unit_id"
                            ],
                            "variable": [
                                {
                                    "key": "unit_id",
                                    "value": "8e4b2a17-5c39-4d61-b0f2-7a9c3e51d804",
                                    "description": "Maat's identifier for the physical unit."
                                }
                            ]
                        },
                        "description": "One physical unit: how many it sleeps, in what, its door times, its amenities and\nthe host's answer to every house rule.\n\nA `403` on this operation carries one of two codes — `insufficient_scope` (the\nkey lacks `catalog:read`) or `workspace_scope_mismatch` (a sandbox/production\nmismatch). Branch on `code`.\n\n`house_rules` carries only rules somebody actually answered. A rule that is absent\nwas never asked — it does not mean \"no\".\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Change what a unit is",
                    "request": {
                        "method": "PATCH",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/units/:unit_id",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "units",
                                ":unit_id"
                            ],
                            "variable": [
                                {
                                    "key": "unit_id",
                                    "value": "8e4b2a17-5c39-4d61-b0f2-7a9c3e51d804",
                                    "description": "Maat's identifier for the physical unit."
                                }
                            ]
                        },
                        "description": "Changes what a unit **is**: how many it sleeps, in what, whether it is the whole\nproperty or a room inside one, and its door times.\n\n**Partial.** An absent field is untouched — there is no merge engine and nothing\nhere is nullable, because every one of these columns is required on the platform.\nSend at least one field: an empty body is a `400`, not a silent no-op that reads\nas success.\n\nThe counts are **integers**. A `bathrooms` of `2.5` is refused rather than\ntruncated into a different property than you described; half-bathrooms are not\nexpressible here.\n\nA `403` on this operation carries one of two codes — `insufficient_scope` (the\nkey lacks `catalog:write`) or `workspace_scope_mismatch` (a sandbox/production\nmismatch). Branch on `code`.\n\nSetting `unit_type` to `whole_property` on a property that has another unit is\nrefused with `409 conflict`: the whole property cannot also contain a second one.\n\nThe body is strict. An undeclared field is a `400 validation_failed` naming it,\nnever a silent drop, and a stray query string is refused the same way.\n\nA property with more than one unit, or more than one listing, is refused with\n`409 property_not_simple`; a listing that is not a stay with\n`409 inventory_not_supported`. Nothing is written either way.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"max_guests\": 5,\n  \"beds\": 4,\n  \"check_in_time\": \"15:00\"\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                },
                {
                    "name": "Replace a unit's house-rule answers",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/units/:unit_id/house-rules",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "units",
                                ":unit_id",
                                "house-rules"
                            ],
                            "variable": [
                                {
                                    "key": "unit_id",
                                    "value": "8e4b2a17-5c39-4d61-b0f2-7a9c3e51d804",
                                    "description": "Maat's identifier for the physical unit."
                                }
                            ]
                        },
                        "description": "Replaces a unit's house-rule answers, and optionally its door times.\n\n**You send the rules you decided; we expand the rest.** Every active rule in\nMaat's catalogue that you did not name is written `allowed: false`. That is what\nmakes this a set replace rather than a merge, and it is what keeps a rule with no\nanswer meaning **\"never asked\"** on the read side rather than quietly meaning\n\"no\". Absence has to keep meaning what it says.\n\nAn **empty `answers` array is legitimate**, and it means \"nothing is allowed\". It\nis not the same as never having answered.\n\nA slug the catalogue does not have — **including a retired one** — refuses the\nwhole call with `400`, naming the slugs. `GET /catalog/house-rules` publishes\nevery rule and flags a retired one: keep a retired entry to explain an answer you\nread back, never to write a new one.\n\n**`check_in_time` and `check_out_time` are not rules.** A time is a value, not a\npermission, and both are columns on the unit. They ride this request because they\nare written in the **same transaction** as the answers, so a half-applied edit\nwhere the times saved and the answers rolled back cannot happen. Omit them to\nleave the stored times alone.\n\nAt most a hundred answers, and no repeated slug.\n\nA `403` on this operation carries one of two codes — `insufficient_scope` or\n`workspace_scope_mismatch`. Branch on `code`.\n\nThe body is strict. An undeclared field is a `400 validation_failed` naming it,\nnever a silent drop, and a stray query string is refused the same way.\n\nA property with more than one unit, or more than one listing, is refused with\n`409 property_not_simple`; a listing that is not a stay with\n`409 inventory_not_supported`. Nothing is written either way.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"answers\": [\n    {\n      \"slug\": \"pets_allowed\",\n      \"allowed\": true\n    },\n    {\n      \"slug\": \"smoking_allowed\",\n      \"allowed\": false\n    }\n  ],\n  \"check_in_time\": \"15:00\",\n  \"check_out_time\": \"12:00\"\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                }
            ]
        },
        {
            "name": "listings",
            "description": "The sellable view of a unit — title, rate, badges, and whether it is on sale.\n",
            "item": [
                {
                    "name": "Read one listing",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id"
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "One listing — the sellable view of a unit: its title, its rate, its badges and\nwhether it is on sale right now.\n\n**A listing that is not a stay is returned, with its kind.** `inventory_kind` is\n`unit` for a place somebody sleeps in, and `service` or `experience` for something\nsold by the seat. Answering `404` for those would be false — the listing exists,\nin your workspace. The write contracts are where a non-stay listing is refused.\n\nFor such a listing `publishable` is `null`, meaning **nothing was assessed**\nrather than \"assessed and failed\": the validity gate asks about cover photos and\namenities, and a seat on a walking tour has no answer to that.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Change a listing, and whether it is on sale",
                    "request": {
                        "method": "PATCH",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id"
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "Changes the sellable view of a unit: its title, its description, its badges, and\nwhether it is on sale.\n\n**Partial.** An absent field is untouched. `description` is the one member that\naccepts `null`, because \"this listing has no description\" is a real state you\nmust be able to reach. Send at least one field: an empty body is a `400`.\n\n**`status` is not a column write.**\n\n`listed` runs Maat's own validity gate — a price above zero, at least one\namenity, a cover photo and a minimum photo count — and publishes the listing only\nif it passes. **If it does not pass, the answer is still `200`**: the returned\nlisting's `publishable` and `missing` say why, and **nothing about whether the\nlisting is on sale is changed**. Acceptance is not publication. A listing already\non sale is not taken off it as a side effect of a request that asked for the\nopposite.\n\n`unlisted` holds the listing off sale.\n\nA listing **Maat has suspended for its own reasons** stays off sale through a\n`listed` request. So `publishable` can read `true` while `status` reads\n`unlisted` — that is not a contradiction, and it is not something this operation\ncan clear. Contact the workspace.\n\nText is normalised before it is stored, and its length is re-checked afterwards:\ncontrol characters, zero-width characters and bidirectional-override characters\nare removed, and runs of whitespace are collapsed. A title that normalises to\nnothing is a `400`.\n\nThe body is strict. An undeclared field is a `400 validation_failed` naming it,\nnever a silent drop, and a stray query string is refused the same way.\n\nA property with more than one unit, or more than one listing, is refused with\n`409 property_not_simple`; a listing that is not a stay with\n`409 inventory_not_supported`. Nothing is written either way.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"title\": \"Courtyard flat with the blue door\",\n  \"status\": \"listed\"\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                },
                {
                    "name": "Replace a listing's price",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/pricing",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "pricing"
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "Replaces a listing's price: the nightly rate before any rule, fee or discount,\nand the cleaning fee.\n\n`base_rate.value` must be **greater than zero** — a rate of zero is one of the\nthings the validity gate refuses to publish. `cleaning_fee.value` may be zero.\n\n**One listing, one currency.** `cleaning_fee.currency` must equal\n`base_rate.currency`. A listing carries a single currency, and accepting two\nwould mean silently picking one of them.\n\nSending a currency **different from the stored one is allowed, and it is a\nre-price**: the number is taken at face value in the new currency and nothing is\nconverted. A silent conversion, at some rate on some day, would be a price nobody\nchose.\n\n**A `PUT` replaces.** Omitting `cleaning_fee`, or sending `null`, sets it to\nzero. A read publishes a zero fee as `null`, so a listing read straight back into\nthis write round-trips.\n\nThis is the one operation in the document that requires `pricing:write` rather\nthan `catalog:write`: what a stay COSTS is a different capability from what it\nIS, and a key issued for a catalogue sync should not thereby be able to change a\nnightly rate.\n\nThe body is strict. An undeclared field is a `400 validation_failed` naming it,\nnever a silent drop, and a stray query string is refused the same way.\n\nA property with more than one unit, or more than one listing, is refused with\n`409 property_not_simple`; a listing that is not a stay with\n`409 inventory_not_supported`. Nothing is written either way.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"base_rate\": {\n    \"value\": \"1250.00\",\n    \"currency\": \"EGP\"\n  },\n  \"cleaning_fee\": {\n    \"value\": \"150.00\",\n    \"currency\": \"EGP\"\n  }\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                }
            ]
        },
        {
            "name": "catalog",
            "description": "The reference lists a property push has to speak — property types, amenities and\nhouse rules. The same in every workspace.\n",
            "item": [
                {
                    "name": "Every property type",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/catalog/property-types",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "catalog",
                                "property-types"
                            ]
                        },
                        "description": "Every `property_type` slug `PUT /properties/{provider_id}` accepts.\n\nSlugs only. Every label on this platform is resolved per reader's language, so\npublishing an English string here would hand you a translation that goes stale —\nmap our slug onto your own vocabulary instead.\n\nThe catalogue is the same in every workspace and every environment, so what you\nread in the sandbox is the production answer.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Every amenity",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/catalog/amenities",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "catalog",
                                "amenities"
                            ]
                        },
                        "description": "Every amenity slug we recognise, with the section it belongs to.\n\nUse it to build your mapping before a catalogue sync. A slug we do not recognise\nis echoed back in `unknown_amenities` on a property push rather than silently\ndropped, but reading this list first is cheaper than finding out one property at\na time.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Every house rule",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/catalog/house-rules",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "catalog",
                                "house-rules"
                            ]
                        },
                        "description": "Every house rule in the catalogue, with the two things you need to render one.\n\n`kind` decides how a rule reads. A `permission` has two polarities — \"Pets\nallowed\" and \"No pets\" are the same rule answered differently. A `requirement`\nhas only one: it is shown when it is answered `true` and has no negated form at\nall, because \"No lock up before leaving\" is not a policy anybody has.\n\n`sort_order` groups rules within a `category`; the bands do not overlap, so\nordering by it alone comes out already grouped.\n\n**Retired rules are included, flagged `retired: true`.** Properties answered them\nbefore they were withdrawn and those answers still come back on a property read —\nso you need the entry to explain the slug. Do not offer a retired rule on a new\npush.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                }
            ]
        },
        {
            "name": "photos",
            "description": "A property's photographs: what state each one is in, how to upload more, how to\narrange them, and how to remove one.\n",
            "item": [
                {
                    "name": "List a property's photos",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/properties/:property_id/photos",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "properties",
                                ":property_id",
                                "photos"
                            ],
                            "variable": [
                                {
                                    "key": "property_id",
                                    "value": "3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7",
                                    "description": "Maat's identifier for the property, as returned by `GET /properties`. Not your\nown `provider_id` — the write endpoint is the one keyed on that, which is why\nthis read carries a `by-id` segment.\n"
                                }
                            ]
                        },
                        "description": "Every photo on the property, in the order guests see them. Position `0` is the\ncover.\n\n**This is how you follow an upload.** A photo you presigned appears here\nimmediately as `pending`; it becomes `ready` once we have processed and reviewed\nit, or `rejected` with a reason from a closed vocabulary. Poll this rather than\nassuming a successful upload means a published image — the review happens after\nyou are gone.\n\n`url` is the published image and is `null` in every other state. A photo our\nautomated review declined reads as `rejected`, not `ready`: its file exists and\nno guest will ever be shown it.\n\nA property with more than one unit, or more than one listing, is refused with\n`409 property_not_simple`.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Get upload forms for new photos",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/properties/:property_id/photos/presign",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "properties",
                                ":property_id",
                                "photos",
                                "presign"
                            ],
                            "variable": [
                                {
                                    "key": "property_id",
                                    "value": "3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7",
                                    "description": "Maat's identifier for the property, as returned by `GET /properties`. Not your\nown `provider_id` — the write endpoint is the one keyed on that, which is why\nthis read carries a `by-id` segment.\n"
                                }
                            ]
                        },
                        "description": "Declare the files you are about to upload and receive one signed form each.\n\n**Two steps, and the second one does not come back to us.** You POST the bytes\nstraight to the returned `url` as a multipart form: every field in `fields`\nfirst, verbatim and in order, then a `file` part LAST. Anything after `file` is\nignored. The bytes never pass through this API, which is why the form pins the\ndestination, the exact `Content-Type` and a length range around the `size` you\ndeclared — a file that ships more bytes than it declared is refused by the\nstorage service after you have already sent them.\n\nEach declared file reserves a photo immediately. It appears in\n`GET …/photos` as `pending`, and stays there until we have processed and\nreviewed it. **A successful upload is not a published image**: poll the list.\n\n**This operation is not idempotent and cannot be made so: there is no dedup\nfield on the request.** A retry mints new `photo_id`s, spends the daily photo\nallowance again, and the abandoned rows occupy the property's 30-photo cap for\nup to forty minutes before being swept to `rejected` / `upload_missing`. If a\ncall times out, list the property's photos before retrying.\n\n**Uploading photos does not put a property on sale.** The order is: upload the\nphotos, wait for them to reach `ready`, then send\n`PATCH /v1/listings/{listing_id}` with `status: \"listed\"` — that operation is\nthe one that runs the validity gate, and it is the only one that does. Photos\nare usually what a `missing: [\"photos\"]` was waiting for, but nothing publishes\na property behind your back.\n\n**You do not choose where a photo goes.** There is no object key, no bucket, no\ncategory and no URL field, in either direction. Position decides the rest:\nthe first photo on an empty property becomes the cover, everything after it is\na highlight, and `PUT …/photos/order` is how you change that.\n\n**Two ceilings.** A property holds at most thirty photos, counting the ones\nstill uploading — a batch that would cross it is a `400` and reserves nothing.\nAnd each API key may presign **200 photos per UTC day** — today's default, not\ncontract, and the changelog will say so if it moves. Crossing it is\n`429 photo_quota_exceeded`, which is NOT the hourly `rate_limited` and does not\nclear within the hour. `quota.remaining_today` comes back on every success so\nyou can pace an import instead of discovering the wall.\n\n**Nothing is reserved on any refusal.** The ceilings are checked before a photo\nexists, so a refused call leaves the property exactly as it was.\n\nA property you create with `PUT /properties/{provider_id}` is refused with\n`409 conflict`: that push replaces the property's whole photo set on every call,\nso a photo uploaded here would be silently undone by your next one. Send its\nphoto URLs on the push instead — we fetch them.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"files\": [\n    {\n      \"content_type\": \"image/jpeg\",\n      \"size\": 482113\n    },\n    {\n      \"content_type\": \"image/webp\",\n      \"size\": 210044\n    }\n  ]\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                },
                {
                    "name": "Arrange a property's photos",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/properties/:property_id/photos/order",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "properties",
                                ":property_id",
                                "photos",
                                "order"
                            ],
                            "variable": [
                                {
                                    "key": "property_id",
                                    "value": "3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7",
                                    "description": "Maat's identifier for the property, as returned by `GET /properties`. Not your\nown `provider_id` — the write endpoint is the one keyed on that, which is why\nthis read carries a `by-id` segment.\n"
                                }
                            ]
                        },
                        "description": "Put the property's photos in the order guests should see them. **Index 0 becomes\nthe cover**, everywhere it is shown.\n\n**Send the COMPLETE set of `ready` photos, each exactly once.** A partial list is\nrefused rather than half-applied: the positions of everything you did not\nmention would otherwise have to be invented by us. Omitting one, repeating one,\nor naming one that is not `ready` are all refusals that write nothing, and each\nnames what was wrong.\n\nPhotos that are not `ready` are not orderable — they have no image yet, or they\nhave one nobody may see. They keep their places after the ordered ones and join\nthe sequence when they settle.\n\nAnswers with the whole strip as `GET …/photos` publishes it.\n\nReordering does not change whether the property is on sale, in either\ndirection. Publication is `PATCH /v1/listings/{listing_id}` with\n`status: \"listed\"`.\n\nA property you create with `PUT /properties/{provider_id}` is refused with\n`409 conflict` — your next push would undo the change. Send its photo URLs on the\npush instead.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"photo_ids\": [\n    \"0f2b9c81-4a7e-4d33-9b10-2c7e5a6f1d94\",\n    \"9a4e77b0-1c2d-4e5f-8a91-3b6c0d7e2f45\"\n  ]\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                },
                {
                    "name": "Remove a photo",
                    "request": {
                        "method": "DELETE",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/properties/:property_id/photos/:photo_id",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "properties",
                                ":property_id",
                                "photos",
                                ":photo_id"
                            ],
                            "variable": [
                                {
                                    "key": "property_id",
                                    "value": "3c9d1f42-7b60-4a18-9e55-0d2a6f8c31b7",
                                    "description": "Maat's identifier for the property, as returned by `GET /properties`. Not your\nown `provider_id` — the write endpoint is the one keyed on that, which is why\nthis read carries a `by-id` segment.\n"
                                },
                                {
                                    "key": "photo_id",
                                    "value": "0f2b9c81-4a7e-4d33-9b10-2c7e5a6f1d94",
                                    "description": "Maat's identifier for one photo, as returned by\n`GET /properties/{property_id}/photos`.\n"
                                }
                            ]
                        },
                        "description": "Removes the photo and closes the gap behind it, so positions stay dense.\n\n**If you delete the cover, position 0 of what remains becomes the cover.** That\nis the same rule the Maat apps follow.\n\n`204` whether or not the photo was still there. Deleting something already gone\nis the end state you asked for, so a retried or redelivered call is safe.\n\nDeleting a photo never takes a listing off sale, even if the property drops\nbelow the photo floor as a result — that would be a change you did not ask for.\nNor does any photo operation put one ON sale: publication is\n`PATCH /v1/listings/{listing_id}` with `status: \"listed\"`, which is where the\nvalidity gate runs. `GET /properties/by-id/{property_id}` reports what the\nproperty is missing at any time.\n\nA property you create with `PUT /properties/{provider_id}` is refused with\n`409 conflict` — your next push would undo the change. Send its photo URLs on the\npush instead.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                }
            ]
        },
        {
            "name": "calendar",
            "description": "What each night is doing, and who is holding it — plus the **host blocks** your\nworkspace closes nights with, which are keyed by date rather than by an identifier of\nyours. The blocks your system pushes under its own identifiers are under `blocks`.\n",
            "item": [
                {
                    "name": "Read availability, night by night",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/calendar?from=&to=",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "calendar"
                            ],
                            "query": [
                                {
                                    "key": "from",
                                    "value": "2026-09-01",
                                    "description": "First night of the window, **inclusive**. Send it together with `to`, or send\nneither: one alone is refused rather than half-defaulted.\n",
                                    "disabled": true
                                },
                                {
                                    "key": "to",
                                    "value": "2026-09-30",
                                    "description": "Last night of the window, **inclusive** — unlike a block's `check_out`, which is\nthe exclusive departure day. At most 366 nights from `from`.\n",
                                    "disabled": true
                                }
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "Every night in the window, with what is holding it.\n\n`status` is `available`, `booked`, `held` or `blocked`. A booking and a hold are\ndeliberately different: a hold is a checkout in flight and may expire, a booking\nwill not. `blocked_by` says who put a block there — `host`, `ical` or `partner` —\nand is `null` on every other status, because a sale is not a block.\n\n`external_uid` comes back **only on blocks you pushed** with\n`PUT /blocks/{external_uid}`. A night closed any other way — a host block, or an\nimported calendar feed's event — is reported as blocked with a `null` uid.\n\n`from` and `to` are **inclusive** nights — unlike a block's `check_out`, which is\nthe exclusive departure day. Send both or neither: sending one is refused rather\nthan half-defaulted. Omitting both gives you today plus the next 89 nights; the\nwindow may not exceed 366.\n\n**Live in a sandbox workspace. Returns `501` against a production one, where no\navailability is read and none is disclosed.**\n"
                    },
                    "response": []
                },
                {
                    "name": "What is held right now",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/calendar/holds?page=&page_size=",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "calendar",
                                "holds"
                            ],
                            "query": [
                                {
                                    "key": "page",
                                    "value": "1",
                                    "description": "1-based page number. Defaults to the first page.",
                                    "disabled": true
                                },
                                {
                                    "key": "page_size",
                                    "value": "25",
                                    "description": "Rows per page. The same default and the same ceiling on every list in this API.\n\nA `page_size` above the maximum is **refused** with `validation_failed` naming\nthe parameter — never silently clamped, because a caller who believes they asked\nfor 500 and received 200 will stop walking a set they have not finished.\n",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "Every hold currently holding nights across your workspace, the one that lapses\nsoonest first.\n\nA **hold** is what a guest takes when they reach checkout: the nights come off\nsale on Maat while the payment completes, before there is a booking. It is the\nsame object the\n[`calendar.held`](/api-reference/webhooks/calendar.held) delivery carries, in\nthe same shape — this is the reconciliation surface for it. Ask this when your\nintegration starts up, after an outage, or whenever you want the current answer\nrather than the last one you were pushed.\n\n**An expired hold is never in the result.** The live check is made against the\nclock at the moment you ask, so a hold whose `until` has passed is gone from\nthis list whether or not its `calendar.released` has reached you yet. What you\nget back is what is held now.\n\n**There are no filters, and that is the point.** No `listing_id`, no date\nwindow, no status: the answer is the whole workspace, and any subset would be a\nway to be told less while believing you had been told everything — which is the\ndouble-booking window this contract exists to close. Sending an undeclared\nparameter is `400 validation_failed` naming it, not a silently unfiltered\n`200`.\n\nPaged with `page` and `page_size`; `has_more` tells you whether to ask for the\nnext one. There is no total. Holds appear and vanish in minutes, so a walk over\nseveral pages is a moving set — treat it as a snapshot, not a transaction.\n\nNothing about the guest is here. There is no name, no identifier and no amount:\nsomebody may be about to buy these nights, or may walk away in a minute, and\nwhat you need is that they cannot be sold twice.\n\nA `403` here carries one of two codes — `insufficient_scope` (the key lacks\n`calendar:read`) or `workspace_scope_mismatch`. Branch on `code`.\n\n**Live in a sandbox workspace. Returns `501` against a production one**, where\nnothing is read and no availability is disclosed.\n"
                    },
                    "response": []
                },
                {
                    "name": "Close nights with a host block",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/calendar/blocks",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "calendar",
                                "blocks"
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "Close a set of nights on a listing, the way the Maat app's \"Block dates\" does.\nThe nights need not be contiguous — this is a set of days, not a range — and up\nto 366 of them fit in one request.\n\n**This is not `PUT /v1/blocks/{external_uid}`.** That contract records a night as\nsold in YOUR system, keyed on your own reservation id and releasable only through\nthat id. This one writes a closure of the kind the Maat app makes — maintenance,\nan owner's stay — with a `category`, keyed by date. If you are syncing\nreservations from your own system, `/v1/blocks` is your endpoint; this one is for\nclosing nights by date.\n\n**There is no `note` and no free-text field of any kind.** An unrecognised field\nis a `validation_failed` naming it, never a silent drop — the calendar direction\nof this API carries no guest data, and the obvious thing anybody writes in a note\nis a guest's name.\n\nDates must be today or later, in **UTC**. A property has its own time zone, but a\nbound derived from it would make the same request behave differently for two\nlistings in one account.\n\n**Conflicts refuse the whole range.** If any night you name carries a Maat\nbooking or a live hold, nothing at all is written and the `409` names every\ncolliding night in `conflicts`. We never apply the free part of a range: a\npartial apply you were told succeeded is the one failure you cannot detect.\n\nA night that is already closed by something else — a calendar feed, or a block you\npushed under your own `external_uid` — is **not** a conflict. It is already unavailable, which is\nwhat you asked for, so it comes back under `already_held` instead: no host block\nwas written there, and that closure will lift when its own source lifts.\n\nRe-sending the same nights is a no-op and answers the same receipt.\n\nA listing that sells a room type over several rooms answers\n`409 listing_pooled_unsupported` and nothing is written: a host block here closes\none unit, and such a listing's room is chosen per booking.\n\n**Live in a sandbox workspace. Returns `501` against a production one, where no\nnight is blocked and everything you named stays on sale.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"dates\": [\n    \"2026-09-14\",\n    \"2026-09-15\"\n  ],\n  \"category\": \"maintenance\"\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                },
                {
                    "name": "Release host blocks by date",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/calendar/blocks/release",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "calendar",
                                "blocks",
                                "release"
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "Open nights that a host block is holding.\n\n**A `POST` on its own sub-resource, because it carries a body.** The nights you\nare freeing are a SET, not a range, so they cannot ride in the path or the query;\nand a `DELETE` with a body has no defined meaning in HTTP, so clients, proxies and\nruntimes are free to drop it — which on this operation would turn \"free this one\nnight\" into a request addressed at the whole collection. This operation was a\n`DELETE` on `/calendar/blocks` before that was noticed; it is the same operation\nand keeps the same `operationId`.\n\nIt frees **only** what a host block holds. A night carrying a Maat booking, a live\nhold, a calendar feed's block or a block you pushed under your own `external_uid`\nis skipped in silence: freeing one would put a sold night back on sale. Release\nyour own pushed blocks with `DELETE /v1/blocks/{external_uid}`.\n\n`204` whether or not there was anything to release. Releasing a night that carries\nno host block is the desired end state, not an error to retry.\n\nUnlike the block, there is no date floor here: tidying up a closure from last\nmonth is a legitimate thing to do.\n\n**Live in a sandbox workspace. Returns `501` against a production one, where no\nblock is released and those nights stay unsellable.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"dates\": [\n    \"2026-09-14\",\n    \"2026-09-15\"\n  ]\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                }
            ]
        },
        {
            "name": "blocks",
            "description": "**Partner blocks.** Push the nights your own system already has taken — a stay sold\nthrough your own website, an owner's week — keyed by your system's own `external_uid`,\nand release them again. Only the credential's workspace can release one. Carries no\nguest data of any kind — no name, no booking reference, no amount, no free text.\n\nNot to be confused with the **host blocks** under `calendar`, which are a different\nresource: keyed by date rather than by an identifier of yours.\n",
            "item": [
                {
                    "name": "Hold a range of nights with a partner block",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/blocks/:external_uid",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "blocks",
                                ":external_uid"
                            ],
                            "variable": [
                                {
                                    "key": "external_uid",
                                    "value": "my-res-55123",
                                    "description": "Your own identifier for the block, typically your reservation id. Unique to you.\n"
                                }
                            ]
                        },
                        "description": "Tells Maat that these nights are unavailable, because they are taken on your\nplatform. Creates the block if this `external_uid` is new to us and replaces its\ndates otherwise.\n\n`check_out` is **exclusive** — it is the departure day, not the last night held.\nA stay of `2026-09-01` to `2026-09-05` holds four nights.\n\nIdentify the property with either your own `provider_id` or Maat's `listing_id`,\nand send exactly one of them. Both resolve. Use `listing_id` for a property you\ndid not push — we have no reference of your own for one.\n\n**Nothing about a guest belongs here.** There is no field for a name, a booking\nreference, an amount or a note, and sending one is a `validation_failed` rather\nthan a silent drop. Maat has no relationship with somebody who booked on your\nplatform and has no lawful basis to hold their details.\n\nPushing a block onto nights that already carry a Maat booking or a live hold\nanswers `409`, and **nothing is written**: the whole range is refused rather than\nthe part of it that was free. `conflicts[]` names the colliding dates.\n\nA listing that sells a room type over several rooms answers\n`409 listing_pooled_unsupported`: a block closes one unit, and such a listing's\nroom is chosen per booking. Nothing is written.\n\nRe-sending the same `external_uid` with different dates MOVES the block — the old\nnights are released and the new ones held, in one step. Re-sending it unchanged is\na no-op that answers `updated`.\n\n**Live in a sandbox workspace. Returns `501` against a production one**, after\nvalidating the payload in full — so keep holding these nights on your side until\nthe production contract is enabled.\n\nA `provider_id` is resolved against the mapping **your own** property push\ncreated, inside the workspace your key addresses. Two integrations using the same\nreference for different properties never reach each other's calendars, and a\nreference we have never seen from you is a `404`, exactly like an unknown\nlisting.\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"provider_id\": \"my-prop-9001\",\n  \"check_in\": \"2026-09-01\",\n  \"check_out\": \"2026-09-05\",\n  \"reason\": \"reserved\"\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                },
                {
                    "name": "Release a partner block",
                    "request": {
                        "method": "DELETE",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/blocks/:external_uid",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "blocks",
                                ":external_uid"
                            ],
                            "variable": [
                                {
                                    "key": "external_uid",
                                    "value": "my-res-55123",
                                    "description": "Your own identifier for the block, typically your reservation id. Unique to you.\n"
                                }
                            ]
                        },
                        "description": "Removes the block you pushed under this `external_uid` and frees the nights it\nstill holds.\n\nIt frees **only** the nights that block is holding. A night that is unavailable\nfor another reason — the property's own owner closed it, or a Maat booking has\nsince taken it — stays unavailable. Releasing a block is not the same as\ndeclaring those dates open.\n\nIdempotent: releasing a block that does not exist, or releasing the same one\ntwice, is `204`. There is nothing to retry and nothing has gone wrong.\n\nThere is no partial release. Send the whole block or move it with a `PUT`.\n\n**Live in a sandbox workspace. Returns `501` against a production one**, where no\nblock was ever applied and so none can be released.\n"
                    },
                    "response": []
                },
                {
                    "name": "Read your partner blocks back",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/blocks?from=&to=&page=&page_size=",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "blocks"
                            ],
                            "query": [
                                {
                                    "key": "from",
                                    "value": "2026-09-01",
                                    "description": "Only blocks holding a night on or after this date. A block whose `check_out` is\nthis date holds no night in the window, so it is not returned.\n",
                                    "disabled": true
                                },
                                {
                                    "key": "to",
                                    "value": "2026-09-30",
                                    "description": "Only blocks holding a night on or before this date.",
                                    "disabled": true
                                },
                                {
                                    "key": "page",
                                    "value": "1",
                                    "description": "1-based page number. Defaults to the first page.",
                                    "disabled": true
                                },
                                {
                                    "key": "page_size",
                                    "value": "25",
                                    "description": "Rows per page. The same default and the same ceiling on every list in this API.\n\nA `page_size` above the maximum is **refused** with `validation_failed` naming\nthe parameter — never silently clamped, because a caller who believes they asked\nfor 500 and received 200 will stop walking a set they have not finished.\n",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "Lists the blocks **you** pushed into the workspace your key addresses, newest\nwindow last. It is a reconciliation surface: after a timeout or a `500` you can\nsee whether a push actually landed, rather than guessing.\n\nTwo things bound the result and neither is a parameter you send: the blocks are\nyours, and the workspace is the one your key was issued for.\n\nNothing about a guest is here, because nothing about a guest was ever accepted.\n\nPaged with `page` and `page_size`; `has_more` tells you whether to ask for the\nnext one. There is no total — we do not count the whole set to answer a page.\n\n**Live in a sandbox workspace. Returns `501` against a production one**, where\nnothing has been blocked and so there is nothing to read back.\n"
                    },
                    "response": []
                }
            ]
        },
        {
            "name": "pricing",
            "description": "The quick actions that price and gate a listing, and the rules attached to it.\n",
            "item": [
                {
                    "name": "The quick actions for a listing",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/quick-actions",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "quick-actions"
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "Every quick action this listing can carry, with whatever it currently has\nswitched on.\n\nA quick action is a one-tap pricing or booking rule: a switch plus at most one\ntunable value. `template_key` is a **closed enum** and every member of it is\npermanent — a template that is ever withdrawn keeps its identifier and comes back\n`available: false, unavailable_reason: \"retired\"`, because a published identifier\nnever vanishes.\n\nThere is deliberately **no label**. The names in our own apps are admin copy with\nno translation behind them; render your own from the `template_key`.\n\n`param` describes the value the action takes: `kind`, a `default`, and the bounds\nand `step` where the kind has them. For `kind: \"currency\"` the value is a Money\nobject in the LISTING's currency, and it is the only currency that action can be\nexpressed in.\n\n`available: false` means a `PUT` would be refused right now, and\n`unavailable_reason` says why:\n\n| Reason                | Meaning                                                                      |\n| :-------------------- | :--------------------------------------------------------------------------- |\n| `external_pricing`    | The listing's price is set by another source, so no rule on it would apply.   |\n| `retired`             | The template was withdrawn. Its identifier stays published; `PUT` refuses.    |\n| `inventory_kind`      | The action does not apply to this listing's inventory kind.                   |\n| `insufficient_scope`  | Your key lacks `pricing:write`, so this `pricing.*` row omits `applied`.      |\n\n`warnings` is not decoration. The two families come from two services that deploy\nindependently, so one being unreachable degrades ITS family to nothing and says\nso here — a short list with no warning would be indistinguishable from a listing\nwith nothing switched on.\n\n**Scopes.** Reading needs `rules:write`; there is no read-only scope for rules,\nbecause seeing which rules price your listing is part of managing them. The\n`pricing.*` half additionally needs `pricing:write` — with `rules:write` alone\nyou still get all eight catalogue entries, but every `pricing.*` row comes back\n`applied: null, available: false, unavailable_reason: \"insufficient_scope\"`. The\ncleaning fee a host charges and the surcharges they run are commercial facts\nabout their business, and a key that may not change them is not thereby entitled\nto read them.\n\n**Live in a sandbox workspace. Returns `501` against a production one, where\nnothing is read.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Switch a quick action on, or move its value",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/quick-actions/:template_key",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "quick-actions",
                                ":template_key"
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                },
                                {
                                    "key": "template_key",
                                    "value": "pricing.cleaning_fee",
                                    "description": "The quick action's identifier. A closed set. Permanent: a withdrawn template\nkeeps its identifier and is refused with `validation_failed`, never removed.\n\nSee [`QuickActionTemplateKey`](/api-reference/enums/quick-action-template-key).\n"
                                }
                            ]
                        },
                        "description": "Switch a quick action on, or move an already-applied one to a new value.\nIdempotent: the identifier is in the path, so a retry re-points the same rule\nrather than creating a second one. `201` the first time, `200` after that.\n\nOmit `value` to take the action's own default — you do not have to know the\nnumber to switch something on.\n\n`value` is typed by the action's `param.kind`, and the typing is enforced:\n\n| Kind                     | Value                                                                  |\n| :----------------------- | :--------------------------------------------------------------------- |\n| `number`, `percent`      | a number within `min`–`max`, on `step`                                 |\n| `days_of_week`           | day names from the closed seven, no duplicates                         |\n| `currency`               | a **Money object** whose `currency` equals the listing's               |\n\nA Money object in the wrong currency is a `validation_failed` naming\n`body.value.currency`. There is no conversion on this path and there will not be\none: an amount we converted for you is a number the host never chose, on every\nguest's receipt.\n\n### The scope this needs\n\n`rules:write` for every key, **plus `pricing:write` for a `pricing.*` key**. This\nis the only operation in this document whose requirement depends on the path: a\ncleaning fee changes what a guest pays and a minimum stay does not, so the second\nhalf is checked against your key's scopes when the request arrives and refused\nwith the same `insufficient_scope`, naming what is missing.\n\n### When it is refused\n\n`pricing_mode_conflict` (`409`) — the listing's price is set by another source, so\na rule on it would never take effect. Release that source in the Maat app first.\nNothing is written.\n\n`validation_failed` (`400`) — the value does not suit the action, or the action is\nretired.\n\n**Live in a sandbox workspace. Returns `501` against a production one, where no\nrule is applied and the listing prices exactly as it did before.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"value\": {\n    \"value\": \"200.00\",\n    \"currency\": \"EGP\"\n  }\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                },
                {
                    "name": "Switch a quick action off",
                    "request": {
                        "method": "DELETE",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/quick-actions/:template_key",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "quick-actions",
                                ":template_key"
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                },
                                {
                                    "key": "template_key",
                                    "value": "pricing.cleaning_fee",
                                    "description": "The quick action's identifier. A closed set. Permanent: a withdrawn template\nkeeps its identifier and is refused with `validation_failed`, never removed.\n\nSee [`QuickActionTemplateKey`](/api-reference/enums/quick-action-template-key).\n"
                                }
                            ]
                        },
                        "description": "Switch a quick action off. The rule it created is removed along with it.\n\n`204` whether or not it was applied — switching off something already off is the\ndesired end state, not an error to retry.\n\nTakes the same scopes as the `PUT`, including `pricing:write` for a `pricing.*`\nkey: removing a cleaning fee changes what a guest pays just as surely as adding\none.\n\n**Live in a sandbox workspace. Returns `501` against a production one, where no\nrule is removed.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Every rule attached to a listing",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/rules",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "rules"
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "Read-only. The answer to \"why is my price what it is\": every pricing and booking\nrule attached to this listing, whether it came from a quick action or was drawn\nby hand in the Maat app's rule builder.\n\n`source` tells you which. A `quick_action` rule names the `template_key` it came\nfrom and can be changed through the quick-action endpoints; an `authored` one\ncannot be changed here at all.\n\n**Rule authoring is not on this API, and there is no write here of any kind.**\nNo create, no link, no unlink, no pausing. A rule's logic is a node graph drawn on\na canvas, and publishing that graph would freeze an internal shape as a contract\non the day it shipped — so this list carries the rule's identity and its state,\nand nothing about what it does.\n\n`warnings` follows the same rule as the quick-action list: a family whose service\nis unreachable is reported, not silently omitted.\n\n**The pricing half needs `pricing:write`.** Without it the pricing rules are\nabsent from `items` and `warnings` carries `pricing_withheld` — not\n`pricing_unavailable`, which would send you into a retry loop against a refusal\nno retry can change. A rule's `title` is written by the host, and with `active`\nit says what this workspace charges and when; a key that may not change pricing\nis not thereby entitled to read it. The booking rules are unaffected.\n\n**Live in a sandbox workspace. Returns `501` against a production one, where\nnothing is read.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Read back the rates you sent",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/rates?from=&to=",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "rates"
                            ],
                            "query": [
                                {
                                    "key": "from",
                                    "value": "2026-12-20",
                                    "description": "First night to read, inclusive.",
                                    "disabled": false
                                },
                                {
                                    "key": "to",
                                    "value": "2026-12-31",
                                    "description": "Last night to read, inclusive. At most 366 nights after `from`.",
                                    "disabled": false
                                }
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "The nights **you** sent for this listing, over an inclusive window of at most\n366 nights.\n\nThis is a receipt: nothing on it is something Maat holds that you did not send.\nYou never read a rate this platform derived on its own. If the listing is priced by another source you\nstill read back your own rows — they are simply not in force, which `binding`\ntells you.\n\nA night you never sent is absent, not zero.\n\n**Live in a sandbox workspace. Returns `501` against a production one, where\nnothing is read.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Supply nightly rates for a date range",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/rates",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "rates"
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "Store what each night of a range costs.\n\n`first_night` and `last_night` are **both inclusive and both nights**. They are\nnot called `from`/`to` precisely so there is no exclusive checkout day to\nmisread: `2026-12-20 … 2026-12-31` prices twelve nights, and the twelfth is the\nnight of the 31st.\n\n**Push rates before you take the pricing mode.** That is the recommended order,\nso a listing never spends a window with no price on it. Until the mode is taken\nthe rows are stored and inert, and `binding: false` says so — the request is not\nrefused.\n\n`nightly_rate.currency` **must equal the listing's own currency**. Nothing is\never converted; a mismatch is a `400 validation_failed` naming the field. If the\nhost changes the listing's currency, every future rate you sent is dropped —\nre-push in the new currency. The `currency` on this response is what the\ncalendar now holds, so a push that comes back in a currency you did not send is\nhow you find out.\n\nRanges in one request may **not overlap**, every night must be **today or\nlater** and **no more than 730 nights ahead**, and one request may cover at most\n**730 nights** in total. Two *separate* requests for the same night are fine and\nthe later one wins, which is how a correction works.\n\nThe rates you send are **yours**, and only the calendar of whoever holds the\npricing mode sets what guests are quoted.\n\n`coverage_until` is the last night of the **contiguous** run of YOUR OWN rates\nthat starts today — not the furthest night you have priced. A horizon with a hole\nin it is not covered past the hole, and a night with no rate cannot be quoted at\nall. It reports your calendar whether or not you hold the pricing mode, and\n`GET …/rates` answers the same number.\n\nA listing priced by another source refuses the whole request with\n`409 pricing_mode_conflict`. Nothing is half-applied.\n\n**Live in a sandbox workspace. Returns `501` against a production one, where no\nrate is stored.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"rates\": [\n    {\n      \"first_night\": \"2026-12-20\",\n      \"last_night\": \"2026-12-31\",\n      \"nightly_rate\": {\n        \"value\": \"1450.00\",\n        \"currency\": \"EGP\"\n      },\n      \"min_stay\": 3\n    }\n  ]\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                },
                {
                    "name": "Who prices this listing",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/pricing-mode",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "pricing-mode"
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "What prices this listing right now.\n\n| `mode`     | Meaning                                                          |\n| :--------- | :--------------------------------------------------------------- |\n| `rules`    | Maat's own pricing rules. Your rates, if any, are stored and inert. |\n| `partner`  | You do. `owned_by_you` is `true` and the horizon fields are filled. |\n| `external` | Something else does.                                              |\n\n`external` is deliberately undifferentiated: this API reports that another source\nprices the listing, never which one. You see your own horizon or you see nothing.\n\n`last_received_at` is when **you** last sent a rate for this listing. It is the\nfield to watch: if your credentials are revoked the mode does not silently\nchange hands, so a horizon can run out night by night with nobody re-pushing.\n\n**Live in a sandbox workspace. Returns `501` against a production one, where\nnothing is read.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Become the listing's pricing source",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/pricing-mode",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "pricing-mode"
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "Make your rates the listing's price.\n\nFrom this point the nightly rate a guest is quoted comes from the calendar you\npushed, and the `min_stay` you set on a night binds any stay that includes it.\n\nTaking the mode **switches the listing's own pricing rules off** — Maat's rules\nand your rates would otherwise be two answers to one question. Releasing does\nnot switch them back on; the host re-enables the ones they still want.\n\n**What happens to a night you have not priced is yours to choose**, with\n`fallback_to_base_price`.\n\nLeft `false`, a night with no rate **cannot be quoted**. It is not sold at the\nhost's base price and it is not sold cheaply — it is refused, because a gap\nmeans the feed is broken and guessing a price for it would under-charge a peak\nnight silently. Fill the horizon before you take the mode.\n\nSet `true`, an unpriced night falls back to the host's own base price instead\nof refusing. Choose it when your calendar covers a rolling horizon rather than\nevery night you are contracted for: the listing stays bookable past the end of\nyour feed instead of falling off sale. Your rates still win on every night you\nhave sent one for. A listing whose host has no usable base price is refused\neither way — a fallback that could reach zero would put a stay on sale free.\n\n`warnings` carries `no_rates_next_30_nights` when the coming month is not\nfully priced, under either setting, and taking the mode anyway is allowed:\nbuilding an integration incrementally is a legitimate thing to do.\n\nIdempotent for you. A `409 pricing_mode_conflict` means another source prices\nthis listing — a pricing tool connected in the Maat app. This API does not name\nit; release it from the Maat app first.\n\nOnly nightly-stay listings have a rate calendar; an experience or a service is\n`409 inventory_not_supported`.\n\n**Live in a sandbox workspace. Returns `501` against a production one, where no\nmode is taken and no rule is switched off.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"mode\": \"partner\",\n  \"fallback_to_base_price\": true\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                },
                {
                    "name": "Stop pricing this listing",
                    "request": {
                        "method": "DELETE",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/pricing-mode",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "pricing-mode"
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "Hand the listing back to Maat's own pricing.\n\nYour rate rows are **kept** — they are your submission and your record of what\nyou quoted, and a re-take resumes on them. What changes is that they stop being\nread.\n\nThe listing's own pricing rules are **not** switched back on. Taking the mode\nturned them off; the host decides which to re-enable, because auto-restoring\nwould revive rules they may have been glad to see go.\n\nOwner only, and idempotent: releasing a listing you do not price answers `204`\njust the same. The host can also release from the Maat app at any time, and\ndoes not need your agreement to do it.\n\n**Live in a sandbox workspace. Returns `501` against a production one, where no\nmode is released.**\n"
                    },
                    "response": []
                }
            ]
        },
        {
            "name": "bookings",
            "description": "What is happening on your inventory — never who it is happening to. There is no guest\nidentity anywhere in this tag; a person is always one click away, in the Maat host\nconsole.\n",
            "item": [
                {
                    "name": "List bookings on your inventory",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/bookings?page=&page_size=&status=&listing_id=&check_in_from=&check_in_to=",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "bookings"
                            ],
                            "query": [
                                {
                                    "key": "page",
                                    "value": "1",
                                    "description": "1-based page number. Defaults to the first page.",
                                    "disabled": true
                                },
                                {
                                    "key": "page_size",
                                    "value": "25",
                                    "description": "Rows per page. The same default and the same ceiling on every list in this API.\n\nA `page_size` above the maximum is **refused** with `validation_failed` naming\nthe parameter — never silently clamped, because a caller who believes they asked\nfor 500 and received 200 will stop walking a set they have not finished.\n",
                                    "disabled": true
                                },
                                {
                                    "key": "status",
                                    "value": "confirmed",
                                    "description": "Only bookings in this status.\n\nSee [`BookingStatus`](/api-reference/enums/booking-status).\n",
                                    "disabled": true
                                },
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Only bookings on this listing.",
                                    "disabled": true
                                },
                                {
                                    "key": "check_in_from",
                                    "value": "2026-09-01",
                                    "description": "Only bookings whose `check_in` is on or after this date. Inclusive. Sending a `check_in_from` that is after `check_in_to` is refused with `400 validation_failed` rather than answered with an empty page.",
                                    "disabled": true
                                },
                                {
                                    "key": "check_in_to",
                                    "value": "2026-09-30",
                                    "description": "Only bookings whose `check_in` is on or before this date. Inclusive.",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "One page of what is happening on your inventory, newest first.\n\nThere is NO guest identity anywhere in this response — not a name, not an\ninitial, not an email, not a reference you could join against another read. Every\nfield is a fact about a STAY. `console_url` on each booking is the honest pointer\nto where a person IS seen: the host console, where an authenticated human sees who\nis arriving. That is deliberately slower than a field on this wire, and it is the\npoint — an integration that wants an arrivals board builds one out of the\nplatform's own access control, not out of a JSON key.\n\nFilter with `status`, `listing_id`, `check_in_from` and `check_in_to`; page with\n`page` and `page_size`, and keep asking while `has_more` is `true`. There is no\ntotal. The two date bounds are inclusive, and an inverted range\n(`check_in_from` after `check_in_to`) is refused rather than answered with an\nempty page.\n\nThere is no cancellation over this API, on this operation or any other. Cancelling\na confirmed stay creates refunds and penalties and belongs to a person in the app.\n\nA `403` on this operation carries one of two codes — `insufficient_scope` (the\nkey lacks `bookings:read`) or `workspace_scope_mismatch` (a sandbox/production\nmismatch). Branch on `code`.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Read one booking",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/bookings/:booking_reference",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "bookings",
                                ":booking_reference"
                            ],
                            "variable": [
                                {
                                    "key": "booking_reference",
                                    "value": "5f3a91c2-e8b4-4a71-9c30-1e2f3a4b5c6d",
                                    "description": "Maat's own id for the booking."
                                }
                            ]
                        },
                        "description": "One booking. Addressed by Maat's own `booking_reference` — the id every later\nevent about it carries.\n\n`404 booking_not_found` is ONE answer for \"no such booking\" and \"not in your\nworkspace\" — telling them apart would confirm the existence of another\nworkspace's reservation.\n\nA `403` on this operation carries one of two codes — `insufficient_scope` (the\nkey lacks `bookings:read`) or `workspace_scope_mismatch` (a sandbox/production\nmismatch). Branch on `code`.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Confirm a pending booking",
                    "request": {
                        "method": "POST",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/bookings/:booking_reference/confirm",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "bookings",
                                ":booking_reference",
                                "confirm"
                            ],
                            "variable": [
                                {
                                    "key": "booking_reference",
                                    "value": "5f3a91c2-e8b4-4a71-9c30-1e2f3a4b5c6d",
                                    "description": "Maat's own id for the booking."
                                }
                            ]
                        },
                        "description": "Confirms a `pending` booking — an instant-booking listing never produces one, so\nthis is for a listing that requires host approval. **The body is empty**: there\nis nothing to send beyond the booking you are addressing.\n\n`409 conflict` when the booking is not `pending` — you cannot confirm a booking\nthat is already confirmed, cancelled, or anything else. `404 booking_not_found` is\nONE answer for \"no such booking\" and \"not in your workspace\".\n\nThere is no cancellation over this API. Cancelling a confirmed stay creates\nrefunds and penalties and belongs to a person in the app.\n\nA `403` on this operation carries one of two codes — `insufficient_scope` (the\nkey lacks `bookings:manage`) or `workspace_scope_mismatch` (a sandbox/production\nmismatch). Branch on `code`.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Reject a pending booking",
                    "request": {
                        "method": "POST",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/bookings/:booking_reference/reject",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "bookings",
                                ":booking_reference",
                                "reject"
                            ],
                            "variable": [
                                {
                                    "key": "booking_reference",
                                    "value": "5f3a91c2-e8b4-4a71-9c30-1e2f3a4b5c6d",
                                    "description": "Maat's own id for the booking."
                                }
                            ]
                        },
                        "description": "Rejects a `pending` booking. **The body is empty**: there is nothing to send\nbeyond the booking you are addressing.\n\n`409 conflict` when the booking is not `pending`. `404 booking_not_found` is ONE\nanswer for \"no such booking\" and \"not in your workspace\".\n\nThere is no cancellation over this API. Rejecting a still-pending booking is not\nthe same operation as cancelling a confirmed stay, which creates refunds and\npenalties and belongs to a person in the app.\n\nA `403` on this operation carries one of two codes — `insufficient_scope` (the\nkey lacks `bookings:manage`) or `workspace_scope_mismatch` (a sandbox/production\nmismatch). Branch on `code`.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                }
            ]
        },
        {
            "name": "sandbox",
            "description": "Tools that exist only in a sandbox workspace. Today that is one: a booking fixture,\nwhich mints a synthetic booking on your own sandbox listing so you can watch the real\n`booking.*` webhooks arrive at your endpoint before you go live.\n",
            "item": [
                {
                    "name": "Create a booking in your sandbox",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/sandbox/bookings",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "sandbox",
                                "bookings"
                            ]
                        },
                        "description": "Creates a synthetic booking on one of your **sandbox** listings, so you can watch\nthe real `booking.*` webhooks arrive at your endpoint before you go live.\n\nThe booking is made the way a real one is: the same availability check, the same\nprice quote, the same booking record and therefore the same events, delivered to\nyour endpoint in the same envelope with the same signature. It is not a rehearsal\nof a booking — it is a booking, on inventory that only exists in your sandbox.\n\n**Why you need it.** A sandbox listing is never sold: nothing in a sandbox reaches\na payment provider, so no guest can ever check out on one. Without this operation\n`GET /v1/bookings` would answer an empty page forever, `confirm` and `reject`\nwould have nothing to act on, and the booking webhooks — the larger half of what\nyou build against — could not be seen at all until real money was involved.\n\n**Pick which half of the lifecycle you want.** `status: \"pending\"` mints a booking\nwaiting on a host decision and sends `booking.requested`; you then drive it with\n`POST /v1/bookings/{booking_reference}/confirm` or `/reject` and watch the next\nevent. `status: \"confirmed\"` mints one already confirmed and sends\n`booking.confirmed` straight away.\n\n**What it never does.** There is no guest field, and there will not be one: the\nguest is synthetic, minted by us, and the same one every time for your workspace.\nNo money moves and no payment provider is contacted. Nobody is emailed or\nnotified. The amount on the booking comes from your listing's own price for those\ndates, so the figures have the shape a real booking's do.\n\nIt **holds real nights on your sandbox calendar**, exactly as a real booking\nwould, so a second call over the same dates is refused with `409 conflict`,\nand `conflicts` names every night that is closed — the same member, in the\nsame shape, that `PUT /v1/blocks/{external_uid}` answers a collision with.\nMove the window, or free the nights from the Maat console. A stay may cover at\nmost 30 nights.\n\n**Two codes share `409` here. Branch on `code`, never on the status.**\n`conflict` is retryable with a different window; `sandbox_only` — the listing\nis the one in your LIVE workspace, and the fixture only ever creates bookings\non sandbox inventory — never is. `409 inventory_not_supported` for an\nexperience or a service: this creates stays only.\n\n`404 listing_not_found` for everything else: an id that names nothing, and an\nid that names a listing outside your own live/sandbox pair. Those are one\nanswer on purpose, so the status here cannot be used to find out whether an\nid names another workspace's inventory.\n\n**Live in a sandbox workspace. Returns `501` against a production one**, after\nfull authentication and validation, and nothing is created.\n\nOne more thing has to be true before a delivery leaves the platform: an operator\nenables the outbound delivery job. See\n[Webhooks](/guides/webhooks) for how to tell whether it is on.\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"listing_id\": \"7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24\",\n  \"check_in\": \"2026-09-12\",\n  \"check_out\": \"2026-09-16\",\n  \"guests\": 3,\n  \"status\": \"confirmed\"\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                }
            ]
        },
        {
            "name": "reviews",
            "description": "A listing's reviews in their public form, and the one reply a workspace can leave on\neach.\n",
            "item": [
                {
                    "name": "Read a listing's reviews",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/listings/:listing_id/reviews?page=&page_size=",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "listings",
                                ":listing_id",
                                "reviews"
                            ],
                            "query": [
                                {
                                    "key": "page",
                                    "value": "1",
                                    "description": "1-based page number. Defaults to the first page.",
                                    "disabled": true
                                },
                                {
                                    "key": "page_size",
                                    "value": "25",
                                    "description": "Rows per page. The same default and the same ceiling on every list in this API.\n\nA `page_size` above the maximum is **refused** with `validation_failed` naming\nthe parameter — never silently clamped, because a caller who believes they asked\nfor 500 and received 200 will stop walking a set they have not finished.\n",
                                    "disabled": true
                                }
                            ],
                            "variable": [
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Maat's identifier for the listing."
                                }
                            ]
                        },
                        "description": "One page of a listing's reviews, newest first, in their **public** form — the\nsame shortened reviewer label anyone reading the listing page sees (\"Sarah M.\"),\nnever a full name. The full name stays on the host console, where a person\nauthenticated to see it; there is no field here that carries it.\n\nPaged with `page` and `page_size`; keep asking while `has_more` is `true`. There\nis no total.\n\n`404 listing_not_found` when the listing is not in your workspace.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Reply to a review, as the workspace",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/reviews/:review_id/reply",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "reviews",
                                ":review_id",
                                "reply"
                            ],
                            "variable": [
                                {
                                    "key": "review_id",
                                    "value": "2b1c3d4e-5f60-4718-9a2b-3c4d5e6f7081",
                                    "description": "Maat's id for the review."
                                }
                            ]
                        },
                        "description": "Posts a reply to a review, published as the **workspace** — exactly as the host\napp publishes one. There is no `author` field and there cannot be: letting a\ncaller name an author would put a person's name on public content through an API\nkey.\n\nOne field: `body`, 1 to 2000 characters. The request is strict — an undeclared\nfield is a `400 validation_failed` naming it, never a silent drop.\n\nOne workspace reply per review over this API. `409 conflict` when the review\nalready has one; edit it in the host app instead. `404 review_not_found` is ONE\nanswer for \"no such review\" and \"not in your workspace\".\n\nA `403` on this operation carries one of two codes — `insufficient_scope` (the\nkey lacks `reviews:reply`) or `workspace_scope_mismatch` (a sandbox/production\nmismatch). Branch on `code`.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"body\": \"Thank you for staying with us — glad the street was as quiet as promised!\"\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                }
            ]
        },
        {
            "name": "finance",
            "description": "What your inventory earned, what has been paid out, and what you owe — read-only, per\nsettlement currency, and never converted or added across currencies.\n\nThere is no write here and no finance notification: asking to be paid commits banking\ndetails, and a balance is a position you poll rather than an event.\n",
            "item": [
                {
                    "name": "Read your balance, per currency",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/finance/balance",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "finance",
                                "balance"
                            ]
                        },
                        "description": "Where your money is right now, as one partition per settlement currency.\n\nA workspace has as many balances as it has settled currencies, and they are\nseparate positions — not one figure in a preferred currency. Nothing in this API\nadds one currency to another and nothing converts between them. If you want a\nsingle headline number, convert the partitions yourself, with a rate you chose and\ncan explain.\n\n`available + reserved_for_payout + paid_out_total` is everything that has cleared.\n`pending` is on top of that and has not: it is money a guest has paid, held until\nthe stay starts. `outstanding_penalties` is what you OWE, and it is deliberately\nnot subtracted from any of them — it is settled from released funds, and netting\nit here would make one number mean two things.\n\nA currency you have never traded in is **absent**, not zero. Absence means nothing\nhas ever happened there.\n\n**Live in a sandbox workspace. Returns `501` against a production one.** A sandbox\nnever charges anybody, so it answers `{ \"partitions\": [] }` — the real shape, with\nnothing in it.\n"
                    },
                    "response": []
                },
                {
                    "name": "List what each payment earned you",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/finance/earnings?page=&page_size=&from=&to=&listing_id=",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "finance",
                                "earnings"
                            ],
                            "query": [
                                {
                                    "key": "page",
                                    "value": "1",
                                    "description": "1-based page number. Defaults to the first page.",
                                    "disabled": true
                                },
                                {
                                    "key": "page_size",
                                    "value": "25",
                                    "description": "Rows per page. The same default and the same ceiling on every list in this API.\n\nA `page_size` above the maximum is **refused** with `validation_failed` naming\nthe parameter — never silently clamped, because a caller who believes they asked\nfor 500 and received 200 will stop walking a set they have not finished.\n",
                                    "disabled": true
                                },
                                {
                                    "key": "from",
                                    "value": "2026-09-01",
                                    "description": "Only earnings whose `earned_on` — the stay's check-in date — is on or after this date. Inclusive. Omit for no lower bound.",
                                    "disabled": true
                                },
                                {
                                    "key": "to",
                                    "value": "2026-09-30",
                                    "description": "Only earnings whose `earned_on` is on or before this date. Inclusive. Omit for no upper bound. A `from` after `to`, or a window wider than 731 days, is refused with `400 validation_failed` rather than truncated.",
                                    "disabled": true
                                },
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Only earnings on this listing. Filters inside your own workspace, so a listing that is not yours returns an empty result rather than another workspace's revenue.",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "One row per PAYMENT, newest stay first — the lines behind the `available` and\n`pending` figures on your balance.\n\nOne row is one payment, not one booking. A booking that is later modified takes\nits own additional charge and therefore has its own row, with its own stay date.\n\n**`earned_on` is the stay's CHECK-IN date, not the date the money moved.** June\nrevenue means the stays that happened in June, which is the only bucketing that\nmakes a year-over-year comparison line up seasonally. It is the same convention\nthe Maat host dashboard uses, so the two agree.\n\n`gross` is what the platform booked as YOUR income for that payment — the payout\nplus any cancellation-insurance premium. It is already net of the platform's\ncommission and service fee, so it is not the guest's total; for what a guest paid,\nread `amount_paid` on the booking. `refunded` is what was reversed, positive, and\n`net` is `gross - refunded`.\n\n`status` says how far that payment's money has got, from held in escrow to\ntransferred out. `reserved` means it is committed to an open payout request:\ncleared, but neither withdrawable nor paid.\n\nFilter with `from`, `to` and `listing_id`; page with `page` and `page_size`, and\nkeep asking while `has_more` is `true`. Both date bounds are inclusive, an\ninverted range is refused rather than answered with an empty page, and a window\nwider than 731 days is refused rather than truncated.\n\n**Live in a sandbox workspace. Returns `501` against a production one.** A sandbox\nnever charges anybody, so it answers an empty page.\n"
                    },
                    "response": []
                },
                {
                    "name": "Total your earnings by day, week or month",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/finance/earnings/summary?from=&to=&granularity=&listing_id=",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "finance",
                                "earnings",
                                "summary"
                            ],
                            "query": [
                                {
                                    "key": "from",
                                    "value": "2026-09-01",
                                    "description": "First day of the summary window, inclusive, over `earned_on` — the stay's check-in date. Required: a summary totals everything inside its window in one pass, so an unbounded one would sweep your whole history on every call.",
                                    "disabled": false
                                },
                                {
                                    "key": "to",
                                    "value": "2026-09-30",
                                    "description": "Last day of the summary window, inclusive. A `from` after `to`, or a window wider than 731 days, is refused with `400 validation_failed` rather than truncated.",
                                    "disabled": false
                                },
                                {
                                    "key": "granularity",
                                    "value": "month",
                                    "description": "How to bucket the window.\n\nRequired rather than defaulted: the three bucket the same rows very differently,\nand a caller who did not say which they wanted should be told to say so.\n\nSee [`EarningsGranularity`](/api-reference/enums/earnings-granularity).\n",
                                    "disabled": false
                                },
                                {
                                    "key": "listing_id",
                                    "value": "7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24",
                                    "description": "Only earnings on this listing. Filters inside your own workspace, so a listing that is not yours returns an empty result rather than another workspace's revenue.",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "The same earning lines, totalled — one bucket per period AND per currency.\n\nA month in which you settled in two currencies is TWO buckets, not one sum. There\nis no total across them anywhere in this API.\n\n`period` is the bucket's FIRST day at every granularity, so `month` gives\n`2026-09-01` rather than `2026-09` and one date parser reads all three. A week\nstarts on the Monday.\n\n`bookings` counts the distinct bookings that contributed. A payment we have not\nyet matched to a booking counts in the amounts and not in that number — a window\nof seconds after a payment settles.\n\n`from`, `to` and `granularity` are all REQUIRED. A summary aggregates everything\ninside its window in one pass, so an unbounded one would sweep your whole history\non every call; and the three granularities bucket the same rows very differently,\nso we would rather be told which you want than guess. The window is inclusive at\nboth ends and may span at most 731 days.\n\nUse `listing_id` to narrow to one listing. It filters inside your own workspace,\nso a listing that is not yours returns an empty series rather than somebody\nelse's revenue.\n\n**Live in a sandbox workspace. Returns `501` against a production one.** A sandbox\nnever charges anybody, so it answers an empty series.\n"
                    },
                    "response": []
                },
                {
                    "name": "List your payout requests",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/finance/payouts?page=&page_size=",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "finance",
                                "payouts"
                            ],
                            "query": [
                                {
                                    "key": "page",
                                    "value": "1",
                                    "description": "1-based page number. Defaults to the first page.",
                                    "disabled": true
                                },
                                {
                                    "key": "page_size",
                                    "value": "25",
                                    "description": "Rows per page. The same default and the same ceiling on every list in this API.\n\nA `page_size` above the maximum is **refused** with `validation_failed` naming\nthe parameter — never silently clamped, because a caller who believes they asked\nfor 500 and received 200 will stop walking a set they have not finished.\n",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "Every payout your workspace has requested, newest first, with what happened to it.\n\n`resolved_at` is when it stopped being open — paid, rejected or cancelled — and is\n`null` while it is still `requested`.\n\n**There is no destination on any of these rows.** No withdrawal method, no bank or\nwallet, no IBAN, no transfer tracking number, no requester. Those are your banking\ndetails, and an API key in a config file is not the credential that should be able\nto read them; they stay in the Maat app, behind a person signing in.\n\n**This API cannot request a payout, and will not be able to.** Asking to be paid\ncommits banking details and moves real money — an act a person takes.\n\nA payout is made in exactly one currency; read it from `amount`.\n\n**Live in a sandbox workspace. Returns `501` against a production one.** A sandbox\nnever charges anybody, so there is nothing to pay out and it answers an empty page.\n"
                    },
                    "response": []
                },
                {
                    "name": "See what is due to be released",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/finance/payouts/upcoming",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "finance",
                                "payouts",
                                "upcoming"
                            ]
                        },
                        "description": "The escrowed money on its way to you, grouped by the date it is released and the\ncurrency it settled in.\n\nFunds are released when the stay starts, so `available_on` is a check-in date and\nthe whole answer is in the future. Only stays you have CONFIRMED are counted: a\nrequest nobody has answered is not money on its way.\n\nOne row is one date AND one currency. Two currencies releasing on the same day are\ntwo rows, and the currency is on `amount`.\n\nThis list is **not paged**. It is bounded by the platform rather than by your\nhistory — future release dates on confirmed stays only — so a page parameter\nwould suggest an unbounded set and invite a walk of one.\n\n**Live in a sandbox workspace. Returns `501` against a production one.** A sandbox\nnever charges anybody, so it answers an empty list.\n"
                    },
                    "response": []
                },
                {
                    "name": "List what you owe",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/finance/penalties?page=&page_size=",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "finance",
                                "penalties"
                            ],
                            "query": [
                                {
                                    "key": "page",
                                    "value": "1",
                                    "description": "1-based page number. Defaults to the first page.",
                                    "disabled": true
                                },
                                {
                                    "key": "page_size",
                                    "value": "25",
                                    "description": "Rows per page. The same default and the same ceiling on every list in this API.\n\nA `page_size` above the maximum is **refused** with `validation_failed` naming\nthe parameter — never silently clamped, because a caller who believes they asked\nfor 500 and received 200 will stop walking a set they have not finished.\n",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "Penalties charged to your workspace, newest first — `amount` is what was charged\nand `outstanding` is what is still owed.\n\nA penalty is a LIABILITY, not negative revenue. It is never netted against an\nearning anywhere in this API, and the `outstanding_penalties` figure on your\nbalance is the same set totalled per currency. Settlement happens from released\nfunds as stays check in, from your balance, or by card in the Maat app.\n\n`status` is `partially_settled` while some of it has been paid, and `written_off`\nwhen it was forgiven rather than paid — a decision somebody made, which is why it\nis not folded into `settled`.\n\n**Read-only.** There is no settlement, no payment and no dispute on this API:\neach of those moves money or opens a conversation, and both belong to a person.\n\n**Live in a sandbox workspace. Returns `501` against a production one.** A sandbox\nnever charges anybody, so it answers an empty page.\n"
                    },
                    "response": []
                }
            ]
        },
        {
            "name": "webhooks",
            "description": "Operating the endpoint you registered with `PUT /webhooks/endpoint` from the outside:\nreading it back, rotating its secret, deleting it, and reconciling what we tried to\ndeliver.\n",
            "item": [
                {
                    "name": "Read your endpoint back",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/webhooks/endpoint",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "webhooks",
                                "endpoint"
                            ]
                        },
                        "description": "Reads back the endpoint you registered with `PUT /webhooks/endpoint`, as the\nwhole body — there is no wrapper object.\n\nThe body is `null` — not a `404` — when nothing is registered. An endpoint you\nhave not set up yet is a normal state for a new integration, not an error.\n\n**No secret is ever returned here.** The row stores a generation number, not the\nsecret itself; the value is returned only by the call that mints it — the first\nregistration, or a rotation — and nowhere else, ever.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Register where deliveries go",
                    "request": {
                        "method": "PUT",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/webhooks/endpoint",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "webhooks",
                                "endpoint"
                            ]
                        },
                        "description": "Sets the single HTTPS endpoint Maat delivers to, and the events you want. Sending\nthis again replaces both — there is only ever one endpoint per workspace.\n\n`events` must name at least one event. It used to be documented as accepting an\nempty array to \"pause\" delivery, and it never did that: every registration\nre-arms the challenge, so an empty one would leave your endpoint\n`pending_verification` and subscribed to nothing. `DELETE /v1/webhooks/endpoint`\nis the operation that means stop — it is idempotent, and it keeps your signing\nsecret and your delivery history.\n\n### The challenge round-trip\n\n**Nothing real is delivered until your endpoint proves it is yours.** During this\ncall we POST a signed `endpoint.challenge` delivery to the URL you gave us, and\nyour handler must answer `2xx` with a JSON body echoing the same `challenge`\nstring:\n\n```json\n{ \"challenge\": \"<the value we sent>\" }\n```\n\nEcho it and the endpoint becomes `active`. Anything else — a non-2xx, an\nunreadable body, a different value — leaves it `pending_verification`, and no\nbooking notification is ever sent to it. Call this endpoint again to retry.\n\nThe challenge is signed exactly like a real delivery, so your verification code is\nexercised before your first real payload rather than after it.\n\n**The challenge is re-armed on EVERY call**, including one that does not change\nthe URL. A URL that verified in March is not evidence about the same URL in\nSeptember.\n\n### What we check about your URL\n\n`https` only, on port 443 or 8443, on a publicly resolvable DNS name. We check the\naddress the name actually **resolves to, at the moment we connect** — not just at\nregistration — and we never follow redirects. If a URL is refused you get one\n`validation_failed`; we do not report which check failed.\n\nIf your endpoint's address later stops resolving publicly, we suspend it and stop\ndelivering rather than retrying into it.\n\n### The signing secret\n\nReturned **once**, in the response to this call, and never again — we do not store\nit in a form anything can read back. Re-registering (a corrected path, a new event\nlist) deliberately does NOT mint a new one, so your existing verification keeps\nworking.\n\nIf you lose it or it leaks, rotate it yourself with\n`POST /v1/webhooks/endpoint/rotate-secret`: we issue the next secret and sign\nevery delivery with **both** for an overlap window, so you can deploy on your own\nclock. See `X-Signature-Previous` under `booking.confirmed`.\n\n### Availability\n\n**Live in a sandbox workspace. Returns `501` against a production one**, where\nno endpoint is registered and nothing is delivered.\n\nA `503` means webhook signing is not configured on our side. Nothing was\nregistered; retry.\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"url\": \"https://hooks.example-partner.invalid/maat\",\n  \"events\": [\n    \"booking.confirmed\",\n    \"booking.cancelled\",\n    \"booking.requested\"\n  ]\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                },
                {
                    "name": "Delete your endpoint",
                    "request": {
                        "method": "DELETE",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/webhooks/endpoint",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "webhooks",
                                "endpoint"
                            ]
                        },
                        "description": "Deletes your endpoint. **Idempotent**: deleting one that is already gone is the\nsame `204`, never a `404` — a second delete is not an error, and the answer does\nnot tell you whether there was one, because acting on that is the `404` handling\nidempotency exists to spare you.\n\nThis stops delivery and forgets the event subscription. It does NOT throw away\nthe delivery history — your existing deliveries stay readable through\n`GET /webhooks/deliveries` — and it does not discard the signing secret either,\nso a later re-registration keeps working with what you already have stored.\n\nRe-registering after a delete always re-arms the challenge: a URL that verified\nbefore is not evidence about the same URL now.\n\nDeliveries still queued for the endpoint are parked `failed` with\n`endpoint_removed`. Their bodies are kept, so they can be replayed — but only\nafter you register the endpoint again **and** answer the challenge; until then a\nredrive answers `409 conflict`. Deleting also starts the payload-retention clock\nthose rows were exempt from while they were queued.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Rotate the signing secret",
                    "request": {
                        "method": "POST",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/webhooks/endpoint/rotate-secret",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "webhooks",
                                "endpoint",
                                "rotate-secret"
                            ]
                        },
                        "description": "Issues a new signing secret and starts an overlap window in which we sign every\ndelivery with **both** secrets, so you can deploy your new one on your own clock —\nsee `X-Signature-Previous` under `booking.confirmed`.\n\n**The request body is empty, on purpose.** There is no `overlap_hours`: how long\nwe keep accepting the previous secret is a property of our own dispatcher, not a\ncaller's knob — a caller who could set it to zero would be choosing an outage they\ncannot see coming.\n\n`signing_secret` is returned here, and nowhere else, ever. There is nowhere to\nread it back from afterward, so store it before you do anything else with this\nresponse.\n\n**This call is NOT idempotent, and a second one during the overlap is refused.**\nWe sign with exactly two secrets — your current one and the previous one, until\nits window closes — so rotating again inside that window would evict the secret\nyou are verifying with today, including for deliveries already queued. A retry\nafter a lost response therefore answers `409 rotation_in_progress` with the\ninstant the window closes, and your existing secret keeps working. Read that as\nproof the earlier call landed.\n\n`409 conflict` when there is no endpoint registered to rotate a secret for —\nregister one first with `PUT /webhooks/endpoint`. `503` means webhook signing is\nnot configured on our side; nothing was rotated.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Send yourself a test event",
                    "request": {
                        "method": "POST",
                        "header": [
                            {
                                "key": "Content-Type",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{baseUrl}}/webhooks/endpoint/test",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "webhooks",
                                "endpoint",
                                "test"
                            ]
                        },
                        "description": "Queues **one real delivery** of the named event to your registered endpoint, built\nfrom a fixed, obviously synthetic fixture — so you can exercise your handler end to\nend (signature, envelope, payload shape, your own 2xx) before a real event exists\nto fire.\n\nIt is a real delivery in every way that matters: the same queue, the same\ndispatcher, the same signing with your current secret (and the previous one during\na rotation overlap), the same retry ladder, the same `User-Agent`, and the same\nrow in `GET /webhooks/deliveries` — badged `\"test\": true` there and in the body we\nPOST you. Verify it exactly as you verify everything else.\n\nIt is NOT a real event in two ways you can rely on. The body carries `\"test\": true`\nat the top level, beside `event_id`, on every test delivery and on no real one —\nbranch on it if your handler must not act on the fixture. And the fixture's ids\nare visibly not yours: `00000000-0000-4000-8000-00000000000n`, a `provider_id` of\n`test-property`, a stay a week from now.\n\nThe `data` you receive is produced by **the same projection as the real event**.\nThere is no second serializer for test bodies to drift from the first.\n\n`event_type` must be one your credential can receive, whether or not your endpoint\nis subscribed to it: `400 validation_failed` names the ones that are not. `409\nconflict` when there is no endpoint registered, or it is not `active` — register it\nand answer the challenge first. Each accepted call spends one unit of a per-credential\nallowance over a rolling 24 hours; `429 test_event_budget_exhausted` carries\n`Retry-After`. A refused call spends nothing and queues nothing.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n",
                        "body": {
                            "mode": "raw",
                            "raw": "{\n  \"event_type\": \"booking.confirmed\"\n}",
                            "options": {
                                "raw": {
                                    "language": "json"
                                }
                            }
                        }
                    },
                    "response": []
                },
                {
                    "name": "List what we have tried to deliver",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/webhooks/deliveries?page=&page_size=&status=&event_type=",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "webhooks",
                                "deliveries"
                            ],
                            "query": [
                                {
                                    "key": "page",
                                    "value": "1",
                                    "description": "1-based page number. Defaults to the first page.",
                                    "disabled": true
                                },
                                {
                                    "key": "page_size",
                                    "value": "25",
                                    "description": "Rows per page. The same default and the same ceiling on every list in this API.\n\nA `page_size` above the maximum is **refused** with `validation_failed` naming\nthe parameter — never silently clamped, because a caller who believes they asked\nfor 500 and received 200 will stop walking a set they have not finished.\n",
                                    "disabled": true
                                },
                                {
                                    "key": "status",
                                    "value": "failed",
                                    "description": "Only deliveries in this status.\n\nSee [`WebhookDeliveryStatus`](/api-reference/enums/webhook-delivery-status).\n",
                                    "disabled": true
                                },
                                {
                                    "key": "event_type",
                                    "value": "booking.confirmed",
                                    "description": "Only deliveries of this event type, e.g. `booking.confirmed`.",
                                    "disabled": true
                                }
                            ]
                        },
                        "description": "Lists what we have tried to deliver to your endpoint, newest first — a\nreconciliation surface for the question every webhook integration eventually\nasks: did you actually send the thing I think I'm missing.\n\n**This is envelopes, not bodies.** Each row carries `payload_present` and nothing\nthat could hold the payload itself — a delivery's body is the most sensitive\nthing this platform sends anywhere, so the list stops at whether one is still\nheld. `GET /webhooks/deliveries/{delivery_id}` is the one read that carries the\nbody.\n\nFilter with `status` and `event_type`; page with `page` and `page_size`, and keep\nasking while `has_more` is `true` — there is no total. `retention_days` says how\nlong we keep delivery history, so you read the window from us rather than from a\nnumber in your own notes.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Read one delivery, body included",
                    "request": {
                        "method": "GET",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/webhooks/deliveries/:delivery_id",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "webhooks",
                                "deliveries",
                                ":delivery_id"
                            ],
                            "variable": [
                                {
                                    "key": "delivery_id",
                                    "value": "6f1a2b3c-4d5e-4f60-8718-9a0b1c2d3e4f",
                                    "description": "Our id for the delivery, as returned by `GET /webhooks/deliveries`."
                                }
                            ]
                        },
                        "description": "One delivery, with the body we sent you — the one read in this document where a\nbody appears at all. The delivery IS the response; there is no wrapper object.\n\nThis is not a new disclosure: it is the same bytes, to the same recipient, that\nalready crossed the wire and that your own logs hold. What it adds is the ability\nto reconcile a delivery you think you missed against what we actually sent,\nwithout opening a support ticket to ask an operator to look at a page that\ndeliberately carries no body.\n\n`payload` is the stored string, **verbatim** — not re-serialized from a parsed\nobject. That is deliberate: it is literally what crossed the wire, so you can\nre-verify our signature against it, and re-serializing would silently change key\norder and whitespace and break exactly that. It is `null` in every\n`payload_state` but `held`.\n\n`404 delivery_not_found` is ONE answer for \"no such delivery\", \"another\nworkspace's delivery\" and \"past the retention window\" — telling them apart would\nconfirm the existence of another workspace's integration.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                },
                {
                    "name": "Put a delivery back in the queue",
                    "request": {
                        "method": "POST",
                        "header": [],
                        "url": {
                            "raw": "{{baseUrl}}/webhooks/deliveries/:delivery_id/redrive",
                            "host": [
                                "{{baseUrl}}"
                            ],
                            "path": [
                                "webhooks",
                                "deliveries",
                                ":delivery_id",
                                "redrive"
                            ],
                            "variable": [
                                {
                                    "key": "delivery_id",
                                    "value": "6f1a2b3c-4d5e-4f60-8718-9a0b1c2d3e4f",
                                    "description": "Our id for the delivery, as returned by `GET /webhooks/deliveries`."
                                }
                            ]
                        },
                        "description": "Puts one delivery back at the front of the queue. **The body is empty, on\npurpose**: there is no reason field, no delay, and no list — a redrive addresses\nexactly one delivery, never a batch.\n\nOnly a `delivered` or `failed` delivery can be replayed. `event_id` is\n**unchanged** by a redrive, so your idempotency key survives it — a redrive is a\nretry, not a duplicate booking notification in your own system. The attempt\nseries resets and the delivery goes back to `pending`.\n\n**The body is empty**: a redrive addresses exactly one delivery, and there is no\nreason field, no delay and no list, because a redrive is never bulk.\n\n`409 conflict` when the delivery is already queued, retrying or being sent; when\nits body is gone (erased, or past the payload retention window — there is\nnothing left to redrive); or when your endpoint is not currently active. `detail`\nnames which. Nothing is changed in any of those cases.\n\n`429 rate_limited` when this credential's rolling 24-hour redrive allowance is\nused up. The allowance is per credential and rolls forward continuously rather\nthan resetting at midnight, and we do not publish the figure — it is an\noperational setting we tune, not a contract.\n\n**Live in a sandbox workspace. Returns `501` against a production one.**\n"
                    },
                    "response": []
                }
            ]
        }
    ]
}
