Skip to main content
A listing’s price and its booking constraints come from rules. Most of those rules are drawn by hand, on a canvas, in the Maat app. A small, fixed set of them are quick actions: one-tap switches with at most one number behind them, and they are the whole of what this API can change. There is a second way to price a listing, and it is not a rule at all: supply the nightly rates yourself and become the listing’s pricing source. That is Pricing a listing for a date range, further down.

The eight actions

template_key is a closed enum. There are eight of them, and the list does not grow quietly — it is checked in CI against the platform’s own seeded templates, so this page and the reference cannot describe something we do not run. pricing.long_stay_discount applies at checkout, not on the calendar. A guest browsing nightly prices will not see it; the quote they are charged does. A published identifier is permanent. If a template is ever withdrawn it keeps its template_key, comes back from the list as available: false, unavailable_reason: "retired", and is refused by PUT. Nothing you have stored ever becomes an unrecognised string. There are no labels on this wire. The names you see in the Maat app are our own copy, written for a host looking at a switch, with no translation behind them. Render your own from the template_key.

Typing the value

param on each action tells you what it takes: a kind, a default, and the bounds and step where the kind has them. Omit value on a PUT to take the default — you do not have to know the number to switch something on.
A PUT answers 200 either way, with result saying which happened — created when it switched the action on, updated when it moved one that was already on. The same field the property and block pushes carry; there is no 201 to branch on.

Money, and the currency we will not guess

pricing.cleaning_fee takes an amount, and on this API an amount never travels without its currency:
That currency must equal the listing’s own. Send a different one and you get a validation_failed naming body.value.currency — we do not convert. An amount we converted for you is a number the host never chose, on every guest’s receipt from then on. Read the listing’s currency from GET /v1/listings/{listing_id} or from the param.default this endpoint returns.

Scopes

rules:write reaches all four operations. Anything touching a pricing.* template needs pricing:write as well — on the writes AND on the read. This is the only place in the API where the requirement depends on the path, and the reason is that these two things are not the same act: pricing.cleaning_fee changes what a guest pays, booking.min_stay changes who may book. A key that manages availability rules should not thereby be able to change money. If your key is missing the second scope on a write you get the usual insufficient_scope, naming it. On the reads the same rule applies, and neither errors. On the quick-action list you still get all eight catalogue entries with rules:write alone — the identifiers, kinds, bounds and defaults are the same for every listing on Maat, so there is nothing to withhold. What is withheld is applied on the pricing.* rows: they come back applied: null, available: false, unavailable_reason: "insufficient_scope". On GET .../rules the pricing rows are withheld entirely, and warnings carries pricing_withheld. There is no catalogue half to keep there: a rule is a host-written title and whether it is live, which between them say what this workspace charges and when. Note the distinct warning — it is not pricing_unavailable, which means the pricing service is down and is worth retrying. This one never is. The fee a listing charges and the surcharges it runs are pricing, and a key that may not change them is not thereby entitled to read them. There is no read-only scope for rules. Seeing which rules price your listing is part of managing them, and a published scope is forever.

A quick action edits the rule your workspace already has

A rule row records no author. PUT re-points an existing rule rather than creating a second one — two pricing.cleaning_fee rules would charge a guest twice — so an apply overwrites the number set in the Maat app, and DELETE removes the rule whoever created it. Treat a quick action as the same switch the host app shows, because it is.

When an action is unavailable

available: false means a PUT would be refused right now. unavailable_reason says why:
  • external_pricing — the listing’s price is set by another source. Rules on it would never be evaluated, so every action reads unavailable and PUT answers pricing_mode_conflict. Release that source in the Maat app and they come back.
  • retired — the template was withdrawn. The identifier stays published; PUT answers validation_failed.
  • inventory_kind — the action does not apply to this listing’s kind. Published for completeness; a listing that is not a stay is refused outright with inventory_not_supported before this could appear.
  • insufficient_scope — about your key, not the listing: every pricing.* row reads this when your key lacks pricing:write, and its applied is null. Ask whoever issued the key for one that carries the scope. The booking.* rows are unaffected. Never retry — no repetition of the request changes what your key holds.

A partial list says so

The two families are served by two of our services, which deploy independently. If one is unreachable, its actions are absent from items and warnings says which:
Read warnings before you act on a short list. An empty pricing family and a pricing service that is restarting look identical otherwise — and one of those means “this listing has no surcharges”, which is a very different thing to tell somebody. A family that could not be read is never reported as retired, for the same reason: a five-minute deploy must not look like a permanent withdrawal.

Reading the rules

GET /v1/listings/{listing_id}/rules answers “why is my price what it is”. It lists every rule attached to the listing — quick actions and hand-drawn ones alike — with its title, whether it is active, and its source:
A quick_action rule names the action it came from and can be changed through the endpoints above. An authored one cannot be changed here at all. Without pricing:write the family: "pricing" rows are absent and warnings carries pricing_withheld — the example above is what a key holding both scopes sees. The booking rules come back either way. Do not treat pricing_withheld as an outage: unlike pricing_unavailable beside it, no retry will ever change the answer.

Pricing a listing for a date range

Everything above changes rules that Maat’s own engine then evaluates. This section is the other way to price a listing: you supply the nightly rates, and Maat quotes them. The two are mutually exclusive by design, and the order matters.

Push first, then take the mode

Taking the mode first is allowed and the response says so — warnings carries no_rates_next_30_nights — but it leaves a real listing with nights nobody can book until your first push lands. Fill the horizon, then switch.

first_night and last_night are both NIGHTS

Both inclusive, both nights. They are not called from/to precisely so there is no exclusive checkout day to misread: 2026-12-20 … 2026-12-31 prices twelve nights, and the twelfth is the night of the 31st. Ranges in one request may not overlap, every night must be today or later, and one request covers at most 730 nights in total. Two separate requests naming the same night are fine and the later one wins — that is how a correction works. This PUT merges; it does not replace. A night you do not mention keeps the rate you last sent for it. That is the opposite of every other PUT on this API, where an omitted member is removed, and it is the one place the difference matters: there is no way to clear a night once you have priced it, only to re-price it. If you hold the pricing mode, a night you sent last month is still quoted this month unless you send another rate for it.

What a night with no rate is worth is your choice

This is the single most important thing on this page. By default, once you hold the pricing mode, a night your calendar does not cover is not sold at the host’s base rate and not sold cheaply — it is refused. A gap means the feed is broken, and inventing a price for a broken feed is how a peak night gets sold for a fraction of its worth without anybody noticing. That default suits a feed that covers every night you are contracted for. It is a cliff for one that covers a rolling horizon — the night after your last pushed one falls off sale, and it moves a day closer every day. So the choice is yours, per listing, on the take:
With it on, a night you have not priced is quoted at the host’s own base price instead of refusing. Your rates still win on every night you have sent one for — the base price fills gaps and nothing else, and it carries no minimum stay. A listing whose host has no usable base price is refused either way: a fallback that could reach zero would put a stay on sale free, which is worse than a night that cannot be booked. A PUT states the whole mode, so send the field on every take, not only the one that turns it on — omitting it means false. Changing it on a listing you already price is allowed and takes effect on the next quote; you do not need to release first. Either way, warnings, coverage_until and gap_nights_next_90_days keep telling you the truth about what you have actually sent. Fallback changes what an unpriced night costs, not whether you should care about one. So coverage_until is the last night of the contiguous run that starts today, not the furthest night you have priced. A horizon with a hole in it is not covered past the hole, and gap_nights_next_90_days counts what is missing in the coming quarter. Both are on every push response; watch them. It always describes your own calendar, whether or not you hold the pricing mode — so a fill sent before you take it reports a real horizon rather than null, and GET /v1/listings/{listing_id}/rates answers the same number. Whether those nights are in force is binding and mode, never coverage_until.

One currency, never converted

nightly_rate.currency must equal the listing’s own currency. A mismatch is 400 validation_failed naming the field, and nothing is converted — a rate accepted in one currency and charged in another is a real mis-charge, not a rounding difference. If the host changes the listing’s currency, every future rate you sent is dropped, and the listing stays unquotable until you push again in the new currency. Nothing re-pushes on your behalf. There is no webhook for this yet. Your signal is the push response: send a range and read the currency and binding it comes back with. A push that answers a currency you did not send is a currency change you missed, and coverage_until: null on a listing you had filled is the same fact from the other direction. Poll GET /v1/listings/{listing_id}/pricing-mode on whatever cadence your rate plan changes on.

min_stay rides the quote

A min_stay you set on a night binds any stay that includes it — the maximum over the stay’s nights wins. Guests are refused at checkout with the same message they would get from a host’s own minimum-stay rule; there is nothing extra to configure.

Taking it back

Your rate rows are kept — they are your submission and your record of what you quoted, and a re-take resumes on them. The listing’s own pricing rules are not switched back on; the host re-enables the ones they still want. The host can release at any time, from their own app, without your agreement. They see who prices each listing, how far your horizon reaches and when you last sent a rate. A credential of yours being revoked does NOT release the mode — a listing’s prices must not silently change hands because a key expired — so a quiet feed simply runs the horizon down night by night, which is exactly what the host is looking at. There is no webhook for a release yet either, and — importantly — a release does not start refusing you. After a host takes the listing back, your next push still answers 200, with binding: false and mode: "rules": the rows are stored and inert, exactly as they were before you first took the mode. So branch on binding, not on the status code. An integration that only checks for a 409 will keep pushing into a calendar nobody reads.

Reading it back

GET /v1/listings/{listing_id}/rates?from&to returns your own nights in items, at most 366 per read. It is a receipt: nothing on it is something Maat holds that you did not send. It is not paged — the window is the bound — so there is no has_more on it. GET /v1/listings/{listing_id}/pricing-mode answers rules, partner or external. Here external is a third-party pricer — a pricing source that is neither you nor Maat’s own rule engine. It is the one sense of external in this API that does not mean “yours”: external_uid on a calendar block is your own identifier. All four senses are set beside each other in the glossary. It is undifferentiated: this endpoint never names which source it is. You see your own horizon or you see nothing.

While another source prices the listing

Every write is 409 pricing_mode_conflict and nothing is half-applied. Your own rows stay readable and inert. Quick actions on the same listing are refused with the same code, and the rules list shows them unavailable — a rule under an external pricing source would never be evaluated, so offering the switch would be offering a switch that does nothing.

What is not on this API, and will not be

Rule authoring. A rule’s logic is a node graph drawn on a canvas. Publishing that graph would freeze an internal shape as a contract on the day it shipped, and a contract that carried it would invite writes against it. So there is no create, no link, no unlink and no pausing anywhere here — the rules list carries a rule’s identity and its state, and nothing about what it does. Rule templates. Those are an administrative surface, not an integration one. Turning a rule on or off. active is readable and not writable. A rule paused in the app stays paused. Deleting a rate range. There is no per-range delete. A night is re-priced by sending a new rate for it, or the whole calendar stops being read when you release the pricing mode. A delete would be a third way to make a night unquotable, and two is already one more than an integration should have to reason about. Closing check-in or check-out on a date. Not on the rate calendar. Maat does not enforce those today, and a field you could set expecting enforcement is worse than an absent one.

Current status

Live in a sandbox workspace — the quick actions, the rules read, the rate calendar and the pricing mode alike. Against a production workspace every request is authenticated, rate-limited and validated in full, then answers 501: no rule is applied, no rule is removed, no rate is stored, no mode is taken, and the listing prices exactly as it did before.