API authentication
How to authenticate calls to the Axuz API - session, personal axp_ token, team keys and integration tokens - and which credential each route accepts.
The Axuz API accepts four kinds of credential. Each route declares which ones it accepts (the x-axuz-access field in the reference), and using the wrong credential results in 401 or 403. This page shows how to obtain and send each one and when to choose which.
Overview
| Credential | Format | Who uses it | Status |
|---|---|---|---|
| Session (cookie) | access_token cookie | browser, after login | available |
| Session (bearer) | Authorization: Bearer <JWT> | clients that already logged in | available |
| Personal token | axp_ + 60 characters | a user’s own scripts and CLIs | available |
| Team key | axt_ + 60 characters | team integrations | issuing ready, use under construction |
| Integration token | axz_bot_ + 48 characters | external bots | issued in the Integrations Center; workspace task routes do not accept it yet |
Hosts: the main API lives at https://api.axuz.net (staging; production is axuz.club). Login lives at https://auth-api.axuz.net. Secrets below appear as $AXUZ_TOKEN: keep them in an environment variable or a vault, never in code or in a URL.
Session: cookie and bearer
Whoever logs in at POST /auth/login (auth-api host) receives HttpOnly session cookies, among them access_token and refresh_token, valid across Axuz subdomains (SSO). The browser resends them on its own. Login requires the second factor: the /auth/login response opens an MFA challenge and the session is only full after it. See postAuthLogin and postAuthRefresh.
The same JWT from the access_token cookie is also accepted in the header:
curl
curl -sS https://api.axuz.net/me/notes \
-H "Authorization: Bearer $AXUZ_SESSION_JWT"
Node
const res = await fetch('https://api.axuz.net/me/notes', {
headers: { authorization: `Bearer ${process.env.AXUZ_SESSION_JWT}` },
});
console.log(res.status, await res.json());
The server resolution order is: access_token cookie, then Authorization: Bearer. The session expires; renew it with POST /auth/refresh instead of storing the password.
The X-User-ID header does not authenticate: it exists only as a test shortcut, disabled by default and ignored in production.
Personal token (axp_)
This is the recommended way to automate your own work. It belongs to one user (no team), carries scopes and has its own request limit.
POST/me/api-tokens
Only a session creates, lists or revokes personal tokens. An axp_ token can never manage tokens (not even create another), so a leaked token cannot multiply itself. Create one in the account settings or with a session:
curl
curl -sS -X POST https://api.axuz.net/me/api-tokens \
-H "Authorization: Bearer $AXUZ_SESSION_JWT" \
-H "Content-Type: application/json" \
-d '{"name":"nightly backup","scopes":["me:read"],"expires_in_days":90,"rate_limit_per_min":120}'
Node
const res = await fetch('https://api.axuz.net/me/api-tokens', {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.AXUZ_SESSION_JWT}`,
'content-type': 'application/json',
},
body: JSON.stringify({ name: 'nightly backup', scopes: ['me:read'], expires_in_days: 90 }),
});
const created = await res.json(); // created.token only appears in this response
| Field | Rule |
|---|---|
name | required, up to 100 characters |
scopes | at least one; see the table below |
expires_in_days | optional, 1 to 1825 (5 years); omitted = never expires |
rate_limit_per_min | optional, 1 to 1000; default 60 |
The response is 201 with id, name, prefix (for example axp_AbCd, only so you can recognize the token in the list), scopes, rate_limit_per_min, expires_at and the token field, the full secret. The secret appears only once; the server keeps only its SHA-256 hash. If you lose it, revoke and create another: there is no editing of scopes or limit.
Using the token
curl -sS https://api.axuz.net/me/tasks -H "Authorization: Bearer $AXUZ_TOKEN"
The token is valid only in an Authorization: Bearer header (never in a query string, so it does not leak into logs and history) and only on the /me/* routes marked in the reference as “session or PAT”, such as /me/notes, /me/tasks, /me/files, /me/time and /me/notifications. Profile, avatar, GDPR and token management stay session-only.
Scopes
| Scope | Allows |
|---|---|
me:read | GET, HEAD and OPTIONS on the routes above |
me:write | POST, PATCH, PUT and DELETE on the routes above |
me:profile | reserved for sensitive profile data; today no token route uses it |
To read and write, request both scopes: me:write does not imply me:read. Creation accepts unknown scopes (forward compatibility), but they authorize nothing. Without the scope the response is 403 with {"error":"scope required: me:read"}.
Rate limit
Each token has a per-minute bucket. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (epoch seconds of the next minute). When exceeded, the response is 429 with Retry-After and the body {"error":"rate_limited","limit":60,"count":61,"retry_after":23}. Wait for Retry-After before retrying. If the counter (Redis) is down, the API lets requests through instead of taking your integration down; in environments configured as fail-closed it returns 503 rate_limiter_unavailable.
Expiry and revocation
An expired or revoked token answers 401. Revoke with DELETE /me/api-tokens/{tokenID} (204; 404 if it does not exist or was already revoked, without telling the cases apart). Revocation is published to all instances; if that publish fails, a revoked token may remain valid for up to 30 seconds on another instance. The list (GET /me/api-tokens) shows last_used_at, which can lag by up to 30 seconds.
Team keys (axt_)
POST/teams/{teamId}/api-keys
A team key belongs to the team, not to a person. Whoever holds the team:api_keys:manage permission (administrators) creates, lists, edits (name, scopes, expiry) and revokes at /teams/{teamId}/api-keys, always with a session. The create body is the same as the personal token’s, with a default limit of 120 per minute. The secret (axt_ and 60 characters) also appears only once. Scopes come from the public catalog at GET /v1/scopes: forms:read, forms:write, forms:submissions:read, tasks:read, tasks:write, files:read, files:write, webhooks:manage, mail:send, me:read, me:write, me:profile (exact match, no wildcard).
Integration token (axz_bot_)
External bots (Telegram, Discord, Slack and similar) will use an axz_bot_ token plus 48 characters, issued in the team’s Integrations Center (/teams/{teamId}/integrations). By the contract, it reaches only GET /v1/integration/self and the task, board and comment routes in the integration map; any other route answers 403 before the handler. Integration scopes are tasks:read, tasks:write, boards:read, boards:write, comments:write and integrations:read, and never include deletion. Issuing is live in staging; for the state of use on routes, see the bots page and the integrations reference.
Which route accepts which credential
The reference shows on each operation the x-axuz-access field (class) and the guard detail. Summary of the most common details:
| Guard (detail) | Accepts |
|---|---|
public | no credential |
session JWT (authenticated) | session cookie or bearer |
session or PAT | session or personal axp_ token (with me:read / me:write scope) |
session JWT; team membership (team-member) | session of a team member |
team membership; ... admin (team-admin) | session of a team administrator |
team membership; rbac <permission> (permission) | session of a member holding the cited RBAC permission |
session JWT; platform_roles (platform-admin) | session of a platform operator |
bearer OAuth | access token of an OAuth app (/oauth/userinfo) |
team api-key | mail API key (/v1/send, /v1/usage) of the mail service |
public-signed | HMAC signature or URL with a token in the path (inbound webhooks) |
internal | service-to-service key; not for integrators |
A token’s scopes never widen what the person can already do: the ceiling is always their permission (RBAC and channel or project membership).
Errors
| Status | When | Typical body |
|---|---|---|
401 | credential missing, malformed, expired or revoked | {"error":"unauthorized"} |
403 | valid credential without the scope, without team permission, or restricted session | {"error":"scope required: me:write"} or {"error":"enrollment_required",...} |
404 | another user’s resource (the API does not confirm it exists) | {"error":"token not found"} |
429 | token limit exceeded | {"error":"rate_limited",...} with Retry-After |
What not to do
- Put the token in a URL, in source code, in a log or in a front-end bundle: secrets belong on the server only.
- Use an
axp_token for someone else: it acts as its owner. To act on behalf of third parties, use Login with Axuz. - Send
X-User-IDor auser_idin the body to “choose” the identity: the server ignores it and identifies the user by the verified credential. - Retry without pausing after
429: respectRetry-After.