API tokens and agents

Mint, list, and revoke API tokens, manage agent accounts, and fetch MCP setup snippets through the SAQ REST API; all of these are browser-session only.

Written for
integration-developer
Roles
owner, admin, member, elevated-client-user
Requires
Browser session with a re-authentication in the last ten minutes for minting; Workspace token registry and agent accounts: Owner or Admin
Feature
tokens

An API token is either delegated (minted by a person under Account → API tokens, acting on their behalf) or an agent key (the single live key of an agent account, a token-only workspace member). Tokens can never list, mint, revoke, or rotate tokens: every operation on this page answers 403 forbidden to a Bearer request, and /api/me/* is closed to tokens altogether. The token string is shown once, in the response that creates it.

Scopes are read (always implied), write, and triage. Regular client users cannot mint; elevated client users must name at least one project and may not grant triage. Owners and Admins must hold two-factor authentication; promoting a person to Owner or Admin revokes their delegated tokens.

GET /api/me/tokens

The delegated tokens the signed-in person minted, across every workspace they still belong to. Token: no (browser session only).

Response 200 { "tokens": ApiToken[] }

{
  "tokens": [
    {
      "id": "01924c3e-3c40-7d51-8e62-9f7a8b9c0d1e",
      "name": "Jira sync",
      "prefix": "saq_a1B2c3D4",
      "kind": "delegated",
      "scopes": ["read", "write"],
      "projectIds": ["01924c3e-6b2a-7d11-9d0e-3a1f4b5c6d7e"],
      "cidr": "203.0.113.0/24",
      "expiresAt": "2027-03-19T00:00:00.000Z",
      "lastUsedAt": "2026-09-19T10:15:00.000Z",
      "createdAt": "2026-09-01T09:00:00.000Z",
      "identity": {
        "id": "01924c3e-0004-7000-8000-000000000001",
        "email": "kari@acme.example",
        "name": "Kari Nordmann"
      },
      "workspaceSlug": "acme",
      "workspaceName": "Acme"
    }
  ]
}
Field Meaning
id, name Token id and the name given at minting
prefix The first characters of the token, for recognising it; never the whole token
kind delegated or agent
scopes[] Granted scopes
projectIds[] Projects the token is restricted to; empty means the grantor's full set
cidr IPv4 network the token may be used from, or null
expiresAt Expiry, or null for never
lastUsedAt Last use, updated at most once a minute
identity Whose token it is (id, email, name)
workspaceSlug, workspaceName The workspace (present on this list)

POST /api/me/tokens

Mint a delegated token. Token: no (browser session only). Needs a full login (password, passkey, or SSO), a re-authentication in the last ten minutes, two-factor authentication for Owners and Admins, and an SSO session when the person's domain forces SSO.

Body field (MintTokenRequest) Type Required Validation
name string yes 1–80 characters
workspaceSlug string yes 3–32 characters; a workspace the person belongs to
scopes (read, write, triage)[] yes At least one
projectIds uuid[] no Subset of the person's own projects; required (≥ 1) for elevated client users
cidr string (≤50) or null no IPv4 address or network in CIDR notation
expiresAt datetime or null no Null is never

Response 201 (CreatedApiToken): every ApiToken field plus token, the full secret, shown once:

{
  "id": "01924c3e-3c40-7d51-8e62-9f7a8b9c0d1e",
  "name": "Jira sync",
  "prefix": "saq_a1B2c3D4",
  "kind": "delegated",
  "scopes": ["read", "write"],
  "projectIds": [],
  "cidr": null,
  "expiresAt": null,
  "lastUsedAt": null,
  "createdAt": "2026-09-19T10:40:00.000Z",
  "identity": {
    "id": "01924c3e-0004-7000-8000-000000000001",
    "email": "kari@acme.example",
    "name": "Kari Nordmann"
  },
  "workspaceSlug": "acme",
  "token": "saq_YOUR_TOKEN"
}
Error Status details.reason
reauth_required 403 No recent re-authentication
mfa_required 403 Owner or Admin without two-factor authentication
sso_required 403 Forced-SSO domain without an SSO session
forbidden 403 client_token_forbidden (regular client user), client_triage_scope, a project outside the grantor's memberships, agent accounts, or a Bearer request
read_only 403 The workspace is read-only
not_found 404 Not a member of workspaceSlug

DELETE /api/me/tokens/{id}

Revoke one of the person's own tokens. Token: no (browser session only). Response 200 { "ok": true }; 404 not_found for a token that is not theirs.

GET /api/w/{slug}/tokens

Every token bound to the workspace: live ones and those revoked in the last 30 days. Token: no (browser session only). Owner or Admin.

Response 200 { "tokens": WorkspaceApiToken[] }, each an ApiToken plus revokedAt (datetime or null). Error: forbidden 403.

DELETE /api/w/{slug}/tokens/{id}

Revoke any token in the workspace. Token: no (browser session only). Owner or Admin. Still allowed while the workspace is read-only. Response 200 { "ok": true }. Errors: forbidden 403, not_found 404.

GET /api/w/{slug}/agents

The workspace's agent accounts. Token: no (browser session only). Owner or Admin.

Response 200 { "agents": AgentAccount[] }

Field Meaning
membershipId, identityId The agent's membership and identity ids
email <handle>@<slug>; never a mailbox
name Display name
createdAt Creation time
token The live ApiToken, or null when revoked

POST /api/w/{slug}/agents

Create an agent account and its first key. Token: no (browser session only). Owner or Admin with recent re-authentication. Agents are workspace Members, join every project, and count as workspace users.

Body field (CreateAgentRequest) Type Required Validation
handle string yes Lower case, ^[a-z0-9][a-z0-9._-]{0,63}$, not ending in a separator; becomes <handle>@acme
name string yes 1–120 characters
scopes scope[] yes At least one
cidr string (≤50) or null no IPv4 CIDR restriction
curl -X POST https://saq.no/api/w/acme/agents \
  -H 'Cookie: <browser session>' \
  -H 'Content-Type: application/json' \
  -d '{ "handle": "release-bot", "name": "Release bot", "scopes": ["read", "write"] }'

Response 201 (CreatedApiToken) with kind: "agent" and the one-time token. Errors: reauth_required 403, forbidden 403 (admin_only), seat_limit 409, conflict 409 (handle taken).

POST /api/w/{slug}/agents/{identityId}/rotate

Revoke the agent's current key and issue a new one with the same name, scopes, CIDR, expiry, and projects. Token: no (browser session only). Owner or Admin with recent re-authentication. Response 201 (CreatedApiToken). Errors: reauth_required 403, forbidden 403, not_found 404.

GET /api/w/{slug}/mcp

Copy-and-paste MCP client configuration for this workspace, as shown under Settings → Channels → MCP setup. Token: no (browser session only).

Response 200 (McpSetup)

{
  "url": "https://saq.no/mcp",
  "workspace": "acme",
  "claude": "{\n  \"mcpServers\": {\n    \"saq\": {\n      \"type\": \"http\",\n      \"url\": \"https://saq.no/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer saq_YOUR_TOKEN\" }\n    }\n  }\n}",
  "cursor": "{\n  \"mcpServers\": {\n    \"saq\": {\n      \"url\": \"https://saq.no/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer saq_YOUR_TOKEN\" }\n    }\n  }\n}"
}

claude and cursor are JSON strings ready to paste into Claude Code and Cursor; replace saq_YOUR_TOKEN with a real token. See MCP.