Every endpoint outside /auth/* and /status/* requires an Authorization header. There are no API keys and no query-string credentials.
Authorization: Bearer <token>
| Credential | Lifetime | Behaviour |
|---|---|---|
token | 30 minutes | Access token. Sent on every request. Cannot be revoked individually. |
refresh_token | 30 days | Exchanged for a new access token. Rotated on every use: the one you present is revoked and a new one issued. |
Endpoints#
Registration also creates the organization and makes the caller its owner. Everything else in the API is scoped to that organization; there is no cross-organization read of any kind.
Refreshing#
Refresh when a call returns 401, not on a timer. Thirty minutes is the current access-token lifetime and it is configuration, not contract.
curl -X POST "$BASE/auth/refresh" \ -H 'Content-Type: application/json' \ -d '{"refresh_token":"9f2c…"}' # → {"token": "…", "refresh_token": "…"} ← store BOTH
async function apiWithRetry(path: string, init: RequestInit = {}) { let res = await raw(path, init) if (res.status === 401) { const r = await fetch(`${BASE}/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: refreshToken }), }) if (!r.ok) throw new Error('session ended, sign in again') // Both values are replaced. Keeping the old refresh token // guarantees a 401 on the next refresh: it is now revoked. ;({ token, refresh_token: refreshToken } = await r.json()) res = await raw(path, init) } return res }
def refresh(): global token, refresh_token r = requests.post(f"{BASE}/auth/refresh", json={"refresh_token": refresh_token}, timeout=30) if r.status_code != 200: raise RuntimeError("session ended, sign in again") # Both values are replaced. Keeping the old refresh token # guarantees a 401 next time: it is now revoked. data = r.json() token = data["token"] refresh_token = data["refresh_token"] s.headers["Authorization"] = f"Bearer {token}"
Because the presented refresh token is revoked as it is used, a stolen token that the attacker redeems will make the legitimate client's next refresh fail. Store one refresh token, replace it atomically, and never retry a refresh with a value that has already been sent.
Why a request returns 401#
detail | Meaning |
|---|---|
| Invalid email or password | Credentials rejected at login. Deliberately does not distinguish an unknown account from a wrong password. |
| User not found | Token decoded, but the account is gone or has been deactivated. |
| Session ended. Please sign in again. | The account ended all sessions after this token was issued. Refreshing will not help, re-authenticate. |
| Invalid or expired refresh token | The refresh token was already used, revoked, or is older than 30 days. |
A 403 is a different situation entirely: the session is valid and re-authenticating changes nothing. Those carry a machine-readable code. See error codes.