curl --fail-with-body -X POST https://maatstays.wtf/v1/token \
-H "Authorization: Bearer $MAAT_PARTNER_KEY"const res = await fetch('https://maatstays.wtf/v1/token', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.MAAT_PARTNER_KEY}` },
});
if (!res.ok) {
// One indistinguishable 401 covers an unknown key, a revoked key and a
// wrong secret. Do not branch on it.
throw new Error(`token exchange failed: ${res.status}`);
}
const { access_token: token, expires_in: expiresIn } = await res.json();
console.log(token.slice(0, 12), expiresIn);
import os
import requests
res = requests.post(
"https://maatstays.wtf/v1/token",
headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_KEY']}"},
timeout=30,
)
if res.status_code != 200:
raise RuntimeError(f"token exchange failed: {res.status_code}")
body = res.json()
print(body["access_token"][:12], body["expires_in"])
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
type token struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
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 token
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
log.Fatal(err)
}
fmt.Println(body.AccessToken[:12], body.ExpiresIn)
}
{
"access_token": "eyJhbGciOiJIUzI1NiIs…",
"token_type": "Bearer",
"expires_in": 900,
"workspace_id": "3f6b0f2e-1c3a-4d5e-9a70-8b2c4d6e0f11",
"sandbox": true,
"scope": [
"partner:token.issue",
"catalog:read",
"calendar:read"
]
}{
"type": "https://developer.maatstays.wtf/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid API key",
"instance": "urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33",
"code": "unauthorized"
}{
"type": "https://developer.maatstays.wtf/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "Insufficient scope",
"instance": "urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33",
"code": "insufficient_scope"
}{
"type": "https://developer.maatstays.wtf/errors/rate-limited",
"title": "Too many requests",
"status": 429,
"detail": "Too many requests for this API key.",
"instance": "urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33",
"code": "rate_limited"
}{
"type": "https://developer.maatstays.wtf/errors/internal-error",
"title": "Internal error",
"status": 500,
"detail": "The request could not be completed.",
"instance": "urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33",
"code": "internal_error"
}{
"type": "https://developer.maatstays.wtf/errors/service-unavailable",
"title": "Service unavailable",
"status": 503,
"detail": "Token issuance is not configured on this platform yet.",
"instance": "urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33",
"code": "service_unavailable"
}Exchange your API key for a token
Swap your long-lived API key (maat_partner_…) for a short-lived bearer token
scoped to exactly one workspace and one environment. Send the token on every
other call in this document.
Tokens live for fifteen minutes by default and never for more than an hour. Mint
one per batch of work, not one per request, and re-exchange on any 401.
The request body is empty, and that is the control rather than a formality.
The workspace, the environment and the scopes on the token all come from the key
you presented. The endpoint declares no fields at all, and anything you send is
deleted before the handler runs — a request carrying a workspace_id or a
sandbox flag mints exactly the token an empty request would. There is nothing
you could send that would change what the token can reach.
Works against both kinds of workspace — sandbox and production alike. It is
how you authenticate, so it answers before anything else does, and it is the one
operation in this document that never returns 501. Read sandbox in the
response to find out which kind of workspace your key was issued for; it is the
only place that answer is published.
curl --fail-with-body -X POST https://maatstays.wtf/v1/token \
-H "Authorization: Bearer $MAAT_PARTNER_KEY"const res = await fetch('https://maatstays.wtf/v1/token', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.MAAT_PARTNER_KEY}` },
});
if (!res.ok) {
// One indistinguishable 401 covers an unknown key, a revoked key and a
// wrong secret. Do not branch on it.
throw new Error(`token exchange failed: ${res.status}`);
}
const { access_token: token, expires_in: expiresIn } = await res.json();
console.log(token.slice(0, 12), expiresIn);
import os
import requests
res = requests.post(
"https://maatstays.wtf/v1/token",
headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_KEY']}"},
timeout=30,
)
if res.status_code != 200:
raise RuntimeError(f"token exchange failed: {res.status_code}")
body = res.json()
print(body["access_token"][:12], body["expires_in"])
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
type token struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
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 token
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
log.Fatal(err)
}
fmt.Println(body.AccessToken[:12], body.ExpiresIn)
}
{
"access_token": "eyJhbGciOiJIUzI1NiIs…",
"token_type": "Bearer",
"expires_in": 900,
"workspace_id": "3f6b0f2e-1c3a-4d5e-9a70-8b2c4d6e0f11",
"sandbox": true,
"scope": [
"partner:token.issue",
"catalog:read",
"calendar:read"
]
}{
"type": "https://developer.maatstays.wtf/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid API key",
"instance": "urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33",
"code": "unauthorized"
}{
"type": "https://developer.maatstays.wtf/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "Insufficient scope",
"instance": "urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33",
"code": "insufficient_scope"
}{
"type": "https://developer.maatstays.wtf/errors/rate-limited",
"title": "Too many requests",
"status": 429,
"detail": "Too many requests for this API key.",
"instance": "urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33",
"code": "rate_limited"
}{
"type": "https://developer.maatstays.wtf/errors/internal-error",
"title": "Internal error",
"status": 500,
"detail": "The request could not be completed.",
"instance": "urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33",
"code": "internal_error"
}{
"type": "https://developer.maatstays.wtf/errors/service-unavailable",
"title": "Service unavailable",
"status": 503,
"detail": "Token issuance is not configured on this platform yet.",
"instance": "urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33",
"code": "service_unavailable"
}Authorizations
Your long-lived API key (maat_partner_…), sent as
Authorization: Bearer <key>. It authorizes exactly ONE thing: exchanging
itself for a token at POST /token. It reaches no workspace and no data.
Store this; mint tokens from it. If it leaks, revoke it — revoking it kills every outstanding token on its next request.
Response
A token, and what it can reach.
Send it as Authorization: Bearer <access_token>. Never store it — store
the key and mint tokens from it.
Always Bearer. Send the token as Authorization: Bearer <access_token>.
"Bearer"Seconds. Fifteen minutes by default, never more than an hour.
900
The one workspace this token addresses. Fixed by the key.
Whether the workspace is a sandbox. false means every contract answers
501 until launch.
The scopes the key held at exchange time, for your own reading. NOT what authorizes a request: scopes are re-read from the key on every call, so a key narrowed after this exchange is narrowed on its next request.
[
"partner:token.issue",
"catalog:read",
"calendar:read"
]