> ## Documentation Index
> Fetch the complete documentation index at: https://developer.maatstays.wtf/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Exchange your API key for a short-lived, workspace-scoped token, and send the token.

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.

<CodeGroup>
  ```bash cURL theme={null}
  # 1. Exchange the key for a token.
  curl -X POST https://maatstays.wtf/v1/token \
    -H "Authorization: Bearer $MAAT_PARTNER_KEY"

  # → 200
  # {
  #   "access_token": "eyJhbGciOiJIUzI1NiIs…",
  #   "token_type": "Bearer",
  #   "expires_in": 900,
  #   "workspace_id": "3f6b0f2e-1c3a-4d5e-9a70-8b2c4d6e0f11",
  #   "sandbox": true
  # }

  # 2. Call the API with the token.
  curl https://maatstays.wtf/v1/blocks \
    -H "Authorization: Bearer $MAAT_PARTNER_TOKEN"
  ```

  ```javascript Node.js theme={null}
  const keyExchange = await fetch('https://maatstays.wtf/v1/token', {
      method: 'POST',
      headers: { Authorization: `Bearer ${process.env.MAAT_PARTNER_KEY}` },
  });

  if (!keyExchange.ok) {
      // One indistinguishable 401 covers an unknown key, a revoked key
      // and a wrong secret.
      throw new Error(`token exchange failed: ${keyExchange.status}`);
  }

  const { access_token: token, expires_in: expiresIn } = await keyExchange.json();

  // Use the token for a batch of work, then let it expire.
  const res = await fetch('https://maatstays.wtf/v1/blocks', {
      headers: { Authorization: `Bearer ${token}` },
  });
  console.log(res.status, expiresIn);
  ```

  ```python Python theme={null}
  import os

  import requests

  exchange = requests.post(
      "https://maatstays.wtf/v1/token",
      headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_KEY']}"},
      timeout=30,
  )
  exchange.raise_for_status()
  token = exchange.json()["access_token"]

  # Use the token for a batch of work, then let it expire.
  res = requests.get(
      "https://maatstays.wtf/v1/blocks",
      headers={"Authorization": f"Bearer {token}"},
      timeout=30,
  )
  print(res.status_code)
  ```

  ```go Go theme={null}
  package main

  import (
      "encoding/json"
      "fmt"
      "log"
      "net/http"
      "os"
  )

  func main() {
      req, _ := http.NewRequest(http.MethodPost,
          "https://maatstays.wtf/v1/token", nil)
      req.Header.Set("Authorization", "Bearer "+os.Getenv("MAAT_PARTNER_KEY"))

      res, err := http.DefaultClient.Do(req)
      if err != nil {
          log.Fatal(err)
      }
      defer res.Body.Close()
      if res.StatusCode != http.StatusOK {
          log.Fatalf("token exchange failed: %d", res.StatusCode)
      }

      var body struct {
          AccessToken string `json:"access_token"`
          ExpiresIn   int    `json:"expires_in"`
      }
      if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
          log.Fatal(err)
      }

      // Use body.AccessToken for a batch of work, then let it expire.
      fmt.Println("token expires in", body.ExpiresIn, "seconds")
  }
  ```
</CodeGroup>

All examples in these docs use `https://maatstays.wtf/v1` — the only environment
this contract is published against today. Which tenancy your requests land in is decided
by your key, never by the hostname.

## 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** — a workspace is one tenancy on
  Maat, the container a portfolio's properties and calendars live in — and is either a
  sandbox key or a production key. Nothing you send can change that — see
  [Sandbox](/guides/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.

## 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, talk to your Maat
contact; the answer is on our side.

<Note>
  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](/guides/errors).
</Note>
