Every request to the REST API under /api/w/{slug}/… and to the MCP endpoint /mcp is authenticated with an API token sent as a bearer token. A token acts as the person or agent account that owns it, never with more permissions than that owner has in the app. This page explains the two kinds of token, how to get one, and the rules that apply while you use it.
Two kinds of token
| Kind | Owner | Who creates it | Where | Attribution |
|---|---|---|---|---|
| Personal token (delegated) | A person | The person themselves | Account → API tokens | Actions are recorded as the person, with a note that a token acted on their behalf |
| Agent account key | An agent account (a token-only workspace user with the Member role) | An Owner or Admin | Settings → Members → Agent accounts | Actions are attributed to the agent |
An agent account has the address <handle>@<slug>, for example claude@acme. It cannot log in with a password or a magic link, it is a member of every project (including projects created later), and it has exactly one live key at a time. Agent accounts do not use a seat.
Obtain a personal token
- Log in with a full login (password, passkey, or single sign-on). A magic-link session cannot mint tokens.
- Open Account → API tokens.
- Choose the Workspace, give the token a Name, and pick Scopes: Read, Write, Triage (shared Inbox).
- Optionally list Projects (leave empty for every project you belong to), set Allowed addresses (an IPv4 address or prefix), and choose Expires (Never or After N days).
- Select Create token and confirm with your password or passkey. Copy the secret: it is shown once.
Rules at minting time:
- Owners and Admins must have two-factor authentication on the session (TOTP or a passkey).
- If your email domain is forced to single sign-on, the session must have come through that SSO connection.
- Regular client users cannot create tokens ("Regular client users cannot create API tokens."). Elevated client users must name at least one project and cannot grant Triage ("Client users cannot grant triage.").
- A project-scoped token may only name projects you belong to.
- Tokens cannot mint, list, or revoke other tokens.
Obtain an agent account key
- As an Owner or Admin, open Settings → Members and find Agent accounts.
- Enter a Name and a Handle (the address becomes
handle@acme), pick Scopes, and select Create agent. Copy the key: it is shown once. - To replace the key later, select Rotate: the old key is revoked and a new one is issued with the same name, scopes, address restriction, expiry, and project list. Revoke ends the key without a replacement.
Send the token
curl "https://saq.no/api/w/acme/tickets?limit=1" \
-H "Authorization: Bearer saq_YOUR_TOKEN"
Tokens start with saq_. When an Authorization header is present, browser cookies are ignored, and no CSRF token is needed. A token is bound to one workspace: using it with another workspace's slug answers 404 not_found.
Scopes
| Scope | Unlocks |
|---|---|
| read | Always included. Tickets, boards, My work, the workflow, projects, time entries, attachments, and client timesheets the owner can already see. All MCP read tools, including the documentation tools. |
| write | Create and update tickets, comment (with attachments), move a ticket between projects, set the parent, link tickets, add and remove GitHub links, log time, and edit time entries. |
| triage | The shared Inbox: create a ticket without a project, list queue=inbox, and route a ticket out of the Inbox. Workspace users only. |
Scopes only narrow. A write token owned by a Member still cannot change billing fields on a project they are not a member of, because the Member could not do that in the app either.
Project scoping
A token with an empty project list covers every project its owner belongs to, re-evaluated on every request. A token with named projects is limited to those projects, intersected with the owner's current memberships:
- Tickets outside the named projects are invisible (
not_found), even to an Owner or Admin token. - The shared Inbox is never visible to a project-scoped token, whatever its scopes.
- Client timesheet documents follow the same effective scope, and a project-scoped token never receives combined (client-wide) documents.
- If the owner loses a membership, the token loses that project at once.
Address restriction
Allowed addresses accepts one IPv4 address or prefix, for example 203.0.113.10 or 203.0.113.0/24. A restricted token is checked against the client address of every request. If the address cannot be determined, the request is refused: a restricted token fails closed. Requests from another address answer 403 forbidden with the message "This token cannot be used from this address".
Expiry and last use
A token can expire Never or After N days. An expired token answers 401 unauthenticated. The Last used column on Account → API tokens and Settings → API tokens is updated at most once a minute.
Revocation and rotation
- A person revokes their own tokens under Account → API tokens.
- An Owner or Admin sees every token in the workspace under Settings → API tokens (revoked ones stay listed for 30 days) and can revoke any of them.
- Agent keys are rotated or revoked on Settings → Members.
Tokens are also revoked automatically when:
- the owner's role crosses between Member and Owner/Admin (those roles require two-factor authentication at minting), including a change made by SCIM group mapping;
- forced single sign-on starts for the owner's email domain, or the SSO connection becomes Active again while a domain is forced;
- the owner is removed from the workspace, leaves it, or is deactivated by SCIM.
The owner mints a new token afterwards.
Routes a token may call
Tokens reach only /api/w/{slug}/… and /mcp. Anything under /api/me/… answers 403 forbidden ("API keys cannot access global account operations"). Within the workspace, only these routes are open to tokens; every other route answers 403 forbidden with "This operation is not available to API keys".
| Method and path | Scope |
|---|---|
GET /tickets, GET /tickets/{ref} |
read |
GET /board, GET /my |
read |
GET /workflow |
read |
GET /projects, GET /projects/{key} |
read |
GET /time |
read |
GET /documents, GET /documents/{id}/download |
read |
GET /attachments/{id} |
read |
POST /tickets, PATCH /tickets/{ref} |
write |
POST /tickets/{ref}/move, PUT /tickets/{ref}/parent |
write |
POST /tickets/{ref}/comments, POST /tickets/{ref}/attachments |
write |
POST /tickets/{ref}/links, DELETE /tickets/{ref}/links/{linkId} |
write |
POST /tickets/{ref}/github, DELETE /tickets/{ref}/github/{linkId} |
write |
POST /time, PATCH /time/{id} |
write |
Paths are relative to https://saq.no/api/w/{slug}. Not available to tokens: archiving and deleting tickets, viewer grants, time entry deletion and corrections, reports, periods and period close, billing runs, clients and people, workspace settings, the workflow editor, members, authentication settings, and token management. The MCP tools close_period and create_billing_run are refused for the same reason.
Error responses
| Status | error |
When |
|---|---|---|
| 401 | unauthenticated |
Missing, unknown, expired, or revoked token. Message "Log in to continue". |
| 403 | forbidden |
Address outside the token's allowed range; a route not open to tokens; a missing scope ("This token does not include that scope"); an action the owner may not perform. |
| 403 | read_only |
The workspace is read-only because its subscription lapsed. Reads still work. |
| 404 | not_found |
The ticket or project is invisible to the owner, or the slug is not the token's workspace. |
| 429 | rate_limited |
A rate budget is spent; see API conventions. |
SCIM tokens are separate
SCIM bearer tokens start with saq_scim_, are created under Settings → Authentication → SCIM provisioning, and work only on /scim/v2. They cannot call the REST API or MCP, and API tokens cannot call SCIM. See the SCIM 2.0 reference.
In the OpenAPI document
The public OpenAPI document declares the
credential every operation accepts, so most client generators and API
explorers configure the header for you. The scheme names are bearerAuth for
an API key, sessionCookie for the browser session, scimToken for
/scim/v2, and stripeSignature for Stripe's webhook. Which operations
carry bearerAuth is generated from the same allow-list the server enforces,
so the document cannot promise access the API refuses.