Skip to main content
Authentication is one exchange: you hold a long-lived API key (maat_partner_…), you swap it at POST /v1/token for a short-lived bearer token that is scoped to exactly one Maat workspace, and you send that token in the Authorization header on every call. That is the whole model. The exchange has a reference page of its own now, with the full response shape and every error it can return. Its 401 and 503 are problem documents like everything else in this API — see Errors.
All examples in these docs use https://maatstays.wtf/v1 — the one host this contract is published against. Whether your requests land on a sandbox or a production workspace is decided by your key, never by the hostname.

What the response tells you

Read sandbox at least once, in whatever you build. It is how an integration proves it is pointed where its author thought it was, and every 501 in this API is explained by it.

The key

  • Your key looks like maat_partner_a1b2c3d4e5f60718_… and is shown once, when it is issued. Store it in a secret manager; we hold only a hash and cannot show it again.
  • Every key is issued for exactly one workspace — the container a portfolio’s properties and calendars live in — and that workspace is either a sandbox or a production one. Nothing you send can change that — see Sandbox.
  • The key opens exactly one door: the token exchange. It never authenticates a contract endpoint directly.
  • The exchange takes an empty body, on purpose. The workspace, the environment and your identity all come from the key itself; anything you put in the body is ignored.

The token

  • Tokens live for fifteen minutes by default and never more than an hour (expires_in tells you exactly). Mint one per batch of work, not one per request — the exchange has its own, separate rate ceiling.
  • Store the key, mint tokens from it. Never persist a token, and never send one as a cookie or in a query string — the API reads the Authorization header and nothing else.
  • Revocation does not wait for expiry. We re-check the key behind a token on every request, so revoking a key kills its outstanding tokens on their next call.

Scopes

A key is issued with a fixed list of scopes, and every operation in the API reference names the one scope it requires, as x-maat-scope on the operation. It is checked in CI against the route table the gateway enforces, so it cannot silently disagree with what your key needs. x-maat-scope is the floor. Four operations have a second requirement that depends on the path rather than on the operation, and the whole vocabulary — all eighteen scopes, a least-privilege table by job, and what an issuer must hold to grant each one — is Scopes. A token minted from a key that lacks the scope is refused with 403 insufficient_scope, and unlike the 401 this one names the missing scope in detail: it is your own credential, and the fix is on your side — a key that carries it, which is a new key (Get access).
  • Scopes are fixed when the key is issued. They cannot be widened later; a key that needs more is a new key.
  • They are re-read from the key on every request, like revocation. A key narrowed today is narrowed on its next call, before any outstanding token expires.
  • The token’s scope is what the key held at exchange time, echoed for your own reading — decode it to see what a token was issued for, never to decide what to send.
  • partner:token.issue opens the exchange itself and nothing else. It is on every key; on its own it reaches no operation.
Every key issued before scopes existed was given exactly the scopes that equal what its tokens could already do, so nothing that worked yesterday is refused today.

When it is refused

A refused exchange is one indistinguishable 401 whether the key is unknown, revoked, or the secret is wrong — telling those apart would let anyone probe which credentials exist. If a key that worked yesterday is refused today, the answer is on our side: see Get access.
The token endpoint is the one place in this API that answers refusals with a plain { "message": … } body rather than a problem document. Every other response is application/problem+json — see Errors.