Skip to main content
Maat models inventory in three levels, and the API publishes all three: Today every property you can push has exactly one unit with exactly one listing, so the three levels look like ceremony. They are not: the schema has always been property → units → listings, and the shape is published now so that a property with two room types and three rate plans is an addition later rather than a rename of everything you built against. Everything you book, block or price is addressed by listing_id. A unit id is the platform’s own physical inventory and a channel integration never handles one.

Reading a catalogue

Start with the list. Each entry already carries its units and their listings, so a full sync is one walk rather than one call per property.
Paging is the usual loop — see Pagination. There is no total, and page_size is the same here as on every other list: 25 by default, 200 at most. A property carries its units and their listings inline, so one page is a large document; ask for fewer rather than more if you are walking a big portfolio.

What “your properties” means

The whole workspace your key was issued for — every property in it, including the ones created in the Maat app, which have no provider_id at all. provider_id in a response is always yours. A property you did not push comes back with null. Each property also carries the private delivery fields on its address (building, floor, apartment, directions — what a host typed for the guest who has to find the door), the physical unit_id of each unit, and console_url, a deep link into the Maat host console.

Reading one property

The by-id segment says which identifier the read means. PUT /v1/properties/{provider_id} writes by your reference and GET /v1/properties/by-id/{property_id} reads by ours, and those two are the same URL shape as far as a specification is concerned — so the read says which identifier it means rather than leaving the write’s path to mean two things. property_not_found is one answer for “no such property” and “not yours”. So is listing_not_found, and unit_not_found. We do not tell those apart, because doing so would let anyone confirm the existence of another customer’s inventory.

Is it actually on sale?

Two fields answer that, and they answer different questions. status is what is true right now: listed means a guest can find and book it, unlisted means they cannot — for any reason. publishable and missing say whether the listing satisfies Maat’s own validity gate: photos_processing and photos are separate on purpose: sending more images when the answer was “wait” makes the problem worse. publishable can also be null, which is neither true nor false: it means nothing was assessed. That happens on a listing whose inventory_kind is service or experience — something sold by the seat rather than by the night. The gate asks about a rate, amenities, a cover photo and enough photos, and such a listing has no answer to it. Those listings are returned by GET /v1/listings/{listing_id} with their kind, and are read-only through this contract.

The reference catalogues

Three lists, the same in every workspace, and worth reading once at the start of a sync rather than discovering one property at a time:
  • GET /v1/catalog/property-types — every property_type a push accepts.
  • GET /v1/catalog/amenities — every amenity slug, with its section.
  • GET /v1/catalog/house-rules — every house rule, with its kind and sort_order.
They publish slugs and structure, never labels. Every label on this platform is resolved in the reader’s own language, so an English string here would be a translation that goes stale — map our slug onto your vocabulary instead. Two things about house rules are worth reading before you render one: kind decides how a rule reads. A permission has two polarities — “Pets allowed” and “No pets” are the same rule answered differently. A requirement has only one: it appears when it is answered true and has no negated form at all, because “No lock up before leaving” is not a policy anybody has. Retired rules are included, flagged retired: true. Properties answered them before they were withdrawn and those answers still come back on a property read, so you need the entry to explain the slug — but do not offer one on a new push. A unit’s house_rules carries only rules somebody actually answered. A rule that is absent was never asked. It does not mean “no”.

Reading the calendar

GET /v1/listings/{listing_id}/calendar?from&to gives you one entry per night:
booked and held are both sales rather than blocks, and they are separate because they mean different things to you: a hold is a checkout in flight and may expire; a booking will not. blocked_by (host, ical for a calendar feed subscribed on our side, partner for a block pushed through this API, or null) is null on both, because nobody blocked those nights — somebody bought them. external_uid comes back only on blocks you pushed yourself through PUT /v1/blocks/{external_uid}. A calendar feed’s own identifiers are never published at all. from and to are inclusive nights — unlike a block’s check_out, which is the exclusive departure day. Send both or neither; one alone is refused rather than half-defaulted, because a window you did not ask for is worse than an error. Omitting both gives you today plus the next 89 nights, and the window may not exceed 366.

Edits, not upserts

There are two ways to change a property, and they are for different jobs. PUT /v1/properties/{provider_id} is the whole-property push, keyed on your own reference. It replaces everything — name, type, address, occupancy, rate, amenities, photos — and it is the right call when your own system is the source of truth and you are syncing a catalogue you already hold. Send it whole, every time, and an amenity you drop is an amenity we remove. The six targeted edits change one thing about a property that already exists, keyed on Maat’s own ids: Reach for a targeted edit when the change is small and you know which resource it belongs to — a rate that moved, a door time that changed, a house rule the owner decided. Reach for the whole-property push when you are reconciling a catalogue and would otherwise be firing six calls at one property. PUT /v1/listings/{listing_id}/pricing is the one operation on this API that needs pricing:write rather than catalog:write — what a stay costs is a different capability from what it is, and a key issued for a catalogue sync should not thereby be able to move a nightly rate.

PUT replaces a set. PATCH touches what you name

The method tells you what an omitted field does, and it is the same rule everywhere:
  • A PUT on a sub-resource is a set replace. The address, the amenity set, the house-rule answers, the price — you send the whole thing, and a member you leave out is removed. It is the only version of “update” in which “take this amenity off” is sayable at all. It is also why an omitted building or floor is cleared rather than kept: a stale floor number left under a new street sends a guest to the right building and the wrong door.
  • A PATCH is partial. An absent field is untouched. Send at least one field — an empty body is a 400, not a silent no-op that reads as success.
There is no merge engine on either, and there is nothing in between the two. Every body is strict. A field this API does not declare is a 400 validation_failed naming it, never a silent drop, and a stray query string is refused the same way. Text is normalised before it is stored — control, zero-width and bidirectional-override characters removed, whitespace collapsed — and its length re-checked afterwards, so a title that normalises to nothing is a 400 rather than an empty listing title. Two refusals apply to all six, both 409, both writing nothing: property_not_simple when the property is not one unit with one listing, and inventory_not_supported when the listing is an experience or a service. See Errors.

Photos

A property’s photographs are their own subject — the two ways bytes reach us, the states a photo moves through while you wait, ordering, the cover, and removal. They are on Photos.

Publication is a gate

PATCH /v1/listings/{listing_id} is the only operation that decides whether a listing is on sale, and status there is not a column write. status: "listed" runs Maat’s own validity gate — a rate above zero, at least one amenity, a cover photo and a minimum photo count — and publishes the listing only if it passes. If it does not pass, the answer is still a 200. The listing comes back with publishable: false and missing[] saying exactly what it is waiting for, and nothing about publication changes: a listing that was off sale stays off sale, and a listing that was already on sale is not taken off it as a side effect of a request that asked for the opposite. Acceptance is not publication. Read the response rather than the status line.
status: "unlisted" holds the listing off sale. That one always applies. A platform suspension outranks your request. If Maat has held a listing off sale for its own reasons, a listed request does not lift it: the gate can pass and the listing still comes back status: "unlisted" with publishable: true and an empty missing. That is not a contradiction and it is not something this API can clear — talk to the workspace, which can see why. The same missing vocabulary is the one the property push uses, including photos_processing, which is not a deficiency and clears on its own.

Current status

Reads and edits alike are live in a sandbox workspace and answer 501 against a production one, where nothing is read, nothing is written and nothing is disclosed. A 501 means your integration is correct — see Sandbox.