API reference¶
All responses are JSON. Errors have the form {"detail": "<reason>"};
request-validation errors (422) carry a list of {"loc", "msg", "type"}
objects instead. Timestamps are ISO 8601 in UTC with an explicit +00:00
offset.
There are three groups of endpoints:
| Group | Authentication | Used by |
|---|---|---|
Validation /api/v1/auth/validate |
the API token itself (Authorization: Bearer) |
API gateways and services |
Token management /tokens… |
browser session cookie and CSRF header | the dashboard |
Administration /api/admin/… |
administrator session cookie and CSRF header | the admin panel |
An API token cannot be used to manage tokens or accounts.
Validate a token¶
The scheme name is case-insensitive; no request body is needed and no CSRF token is involved.
| Status | Meaning | Gateway should |
|---|---|---|
200 with "valid": true |
Token is valid now | allow |
401 |
Unknown, revoked, expired or missing token, or deleted account | deny |
429 |
Rate limit reached (Retry-After header) |
deny or retry later |
413, 408 |
Request body too large or too slow | fix the client |
503, 500 |
Service unavailable | deny (fail closed), retry with back-off |
Guidance for gateways:
- Fail closed. Treat everything except a
200with"valid": trueas a rejection. - Cache briefly, if at all. A revoked token keeps working for as long as a gateway caches the positive answer. Keep any cache to seconds and purge it during incident response.
- Exempt the gateway from IP rate limits. Validation is limited per client
address (600 per minute by default). List the gateway's source addresses in
rate_limits.token_validation_exempt. - Never log the token. Log
user_idinstead.
Example with Python:
import httpx
def token_owner(token: str) -> str | None:
response = httpx.post(
"https://tokens.example.org/api/v1/auth/validate",
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
if response.status_code == 200 and response.json().get("valid") is True:
return response.json()["user_id"]
return None # fail closed
Token management¶
These endpoints serve the dashboard. They need the session cookie set at sign-in
and, for anything but GET, an X-CSRF-Token header equal to the CSRF cookie
(the value is also in the page's <meta name="csrf-token">). Without a session
they answer 401.
| Method and path | Body | Result |
|---|---|---|
GET /tokens |
Your tokens: id, name, created_at, expires_at, last_used_at, revoked |
|
POST /tokens |
{"name": "...", "expires_in_days": 90} or {"name": "...", "expires_at": "<ISO 8601>"} |
The new token including its value (token), shown only now |
POST /tokens/{id}/renew |
optional expires_in_days or expires_at |
New expiry; the value is unchanged. Without a body the original lifetime is applied again |
POST /tokens/{id}/rotate |
optional expires_in_days or expires_at |
A new value (token); the old value stops working. Keeps the expiry unless given |
DELETE /tokens/{id} |
Revokes the token | |
DELETE /tokens/{id}/permanent |
Deletes the token | |
POST /tokens/revoke-all |
Revokes all your tokens and ends all your sessions; returns tokens_revoked |
|
POST /account/delete |
Deletes your account and its tokens; it cannot sign in again until an administrator restores it | |
POST /logout |
Ends the session (503 if the revocation could not be stored) |
expires_in_days is 1–365 and expires_at must lie in the future, within
tokens.max_expiration_days. Supplying both is an
error. Without either, new tokens get tokens.default_expiration_days.
Administration¶
Administrator session plus X-CSRF-Token, as above. Lists are paged with
page (from 1) and page_size (1–100, default 20).
| Method and path | Body | Result |
|---|---|---|
GET /api/admin/tokens |
All tokens with owner name and email | |
DELETE /api/admin/tokens/{id} |
Revokes a token | |
DELETE /api/admin/tokens/{id}/permanent |
Deletes a token | |
DELETE /api/admin/tokens/revoked |
Deletes all revoked tokens | |
GET /api/admin/users |
Accounts: id, email, name, is_admin, created_at, unity_id (OAuth subject), has_password |
|
POST /api/admin/users |
{"email", "name", "password", "is_admin"} |
Creates a local password account |
PATCH /api/admin/users/{id} |
any of name, password (local accounts only), is_admin |
Updates the account; a password or role change ends its sessions |
DELETE /api/admin/users/{id} |
Deletes the account and its tokens and blocks it from signing in again | |
POST /api/admin/users/{id}/revoke-credentials |
Revokes all of the account's tokens and ends its sessions | |
POST /api/admin/users/restore-oauth |
{"subject": "<exact OAuth subject>"} |
Lets a deleted OAuth account sign in again (as a new, empty account) |
The last remaining administrator cannot be demoted or deleted. New and reset passwords need at least 15 characters, at most 72 bytes, and must not be on the configured block list.
Health¶
| Path | Purpose | Response |
|---|---|---|
GET /healthz |
Liveness: the process answers | Always 200 {"status": "ok"} |
GET /readyz |
Readiness: the database answers | 200 {"status": "ready", "rate_limit_storage": "ok"}; "degraded" when Redis is unreachable or full (limits then apply per process); 503 {"status": "unavailable"} when the database is unreachable |
Rate limits¶
Limits are counted in Redis, so they hold across workers and replicas. Every
request first passes a per-address admission budget (rate_limits.preauth,
240 per minute; token validation has its own, rate_limits.token_validation);
routes then apply their own limits. Exceeding a limit answers 429 with
Retry-After. IPv6 clients are counted per /64 network, since one host
usually controls a whole /64. All values are
configurable.
| Budget | Default | Counted per |
|---|---|---|
| Any request except token validation (admission) | 240/minute | client address |
| Pages, sign-in redirect | 120/minute | client address |
| OAuth callback | 60/minute | client address |
| Local password sign-in | 10/minute, and 5/minute per account | client address / account |
| Token reads | 60/minute | user |
| Token changes | 30/minute | user |
| Logout | 30/minute | user |
| Account deletion | 5/minute | user |
| Administration | 120/minute | user |
| Token validation | 600/minute | client address (exempt addresses unlimited) |
Many people can share one address (campus NAT, VPN, a course). If they hit the
per-address limits, raise preauth and pages rather than disabling limits.