Skip to main content
A key is issued with a fixed list of scopes, and every operation’s page in the API reference names the one it requires as 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 carries partner: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.
Both are 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.