x-maat-scope — see
Create or replace a property for
what that looks like on a page. A token minted from a key without it is refused with
403 insufficient_scope, which — unlike the 401 — names the scope you lack, because
that is a fact about your own credential rather than about anybody else’s data.
Scopes are fixed when the key is issued. They cannot be widened later; a key that needs
another one is a new key — see Managing your keys.
The vocabulary
Seventeen scopes. Fifteen of them authorize contract operations; the other two authorize a route with the key itself, and reach no workspace data at all.Contract scopes
The two that authorize a route directly
partner:mcp.read is separate from partner:token.issue on purpose. Reading our
documentation and pushing properties into a workspace are different jobs, and the credential
for the first has to survive a static config file — claude mcp add --header writes a
header once and never refreshes it, so a fifteen-minute token there breaks within the hour.
Ask for a docs key rather than pointing an assistant at your integration’s key; if you point
it at the wrong one you get a 403, which is the system working.
Ask for the least, by job
Pick the row that describes what you are building. Every one of these also carriespartner:token.issue.
A scope you do not ask for is one you cannot be tricked into using, and it is free to ask
for a second key later. The reverse is not free: widening is a re-issue.
The issuance ceiling
Nobody grants authority they do not hold. When a workspace issues its own key, each scope is checked against the issuing member’s own permissions on that workspace, before anything is minted — the Issue key form disables a scope the member cannot grant and says why.catalog:write needs all three because it is one scope over creating, replacing,
editing and publishing — a member who could mint it holding only Edit Property would be
handing a machine two capabilities they do not have themselves.
The four operations whose requirement is not their scope
x-maat-scope is the floor. On four operations there is a second requirement, and it is
the only place in this API where what a call needs depends on the path rather than on the
operation.
The reason the two writes split is that they are not the same act:
pricing.cleaning_fee changes what a guest pays and booking.min_stay changes who may
book. A key issued to manage availability should not thereby be able to move money.
On the two reads nothing errors. The quick-action list still returns 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 there to withhold. What is withheld is
applied on the pricing.* rows, which come back applied: null, available: false,
unavailable_reason: "insufficient_scope". On the rules read the pricing rows are withheld
entirely and warnings carries pricing_withheld: a rule is a host-written title and
whether it is live, which between them say what a business charges and when.
Why two GETs require a write scope
There is no read-only scope for rules, and that is a decision rather than an oversight.
Seeing which rules price a listing is part of managing them, and a published scope is
permanent — a rules:read added now would be in the vocabulary forever, splitting one
capability into two for every key ever issued. If your integration only wants to read them,
ask for rules:write and do not call the writes.
When you are refused
403 insufficient_scope names the missing scope in detail. Nothing was written, and
retrying unchanged can never succeed — scopes are fixed at issue, so the fix is a new key.
One refusal near it that is not a scope problem:
403 workspace_scope_mismatch— a sandbox token against a production workspace, or the reverse. Check which key you exchanged.
403, so branch on code, never on the status line.
One 403 is shaped differently, and it is the one you are most likely to meet first. POST /v1/mcp is authenticated by the key rather than by a token, so a key without
partner:mcp.read is refused by the gateway before the contract’s problem layer runs: the
body is { "message": "Insufficient scope" } — no code, no type, no instance. Handle
that one on the status and the path; everything else on this API answers with a problem
document.