API tokens allow headless and non-browser clients (scripts, voice assistants, IoT devices, smart watches) to authenticate independently of the OAuth2 cookie flow. Tokens are long-lived, user-scoped, and grant the same access as the owning user.
nav_<43-char URL-safe string> — e.g. nav_aB3xYz9WqLmNpQrStUvXyZaBX-Api-Token HTTP header for REST; ?api_token= query parameter for WebSocketrevoked_at timestamp). Revoked tokens immediately return 401.┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Headless │────▶│ X-Api-Token │────▶│ navi_users │ │ client │ │ header or │ │ + api_tokens│ │ │◀────│ ?api_token │◀────│ (PostgreSQL)│ └──────────────┘ └──────────────┘ └──────────────┘
Token resolution flow (navi/auth/deps.py::_resolve_user_from_api_token):
X-Api-Token header or api_token query paramapi_tokens JOIN navi_usersrevoked_at IS NULLUser from navi_users rowUPDATE api_tokens SET last_used_at = NOW()CREATE TABLE api_tokens (
id SERIAL PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES navi_users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
token_prefix TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
last_used_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ
);
| Column | Description |
|---|---|
token_hash |
SHA-256 of the plain token. Never stores plain text. |
token_prefix |
First 12 chars of the token (nav_aB3xYz9W…) for UI identification. |
revoked_at |
Soft-delete marker. NULL = active. |
POST /api-tokensCreate a new API token. Plain token returned only in this response.
Auth: requires authenticated user.
Request
{ "name": "Smart Watch" }
Response 200
{
"id": 1,
"name": "Smart Watch",
"token": "nav_aB3xYz9WqLmNpQrStUvXyZaBCdEfGhIjKlMnOpQrStUvX",
"token_prefix": "nav_aB3xYz9W…",
"created_at": "2026-05-24T10:00:00+00:00",
"last_used_at": null
}
GET /api-tokensList active (non-revoked) tokens for the current user. Does not expose plain tokens.
Auth: requires authenticated user.
Response 200
{
"items": [
{
"id": 1,
"name": "Smart Watch",
"token_prefix": "nav_aB3xYz9W…",
"created_at": "2026-05-24T10:00:00+00:00",
"last_used_at": "2026-05-24T12:00:00+00:00"
}
]
}
DELETE /api-tokens/{token_id}Revoke (soft-delete) a token belonging to the current user.
Auth: requires authenticated user.
Response 204 — no body
Errors
404 — token not found or does not belong to usercurl -H "X-Api-Token: nav_aB3xYz9WqLmNpQrStUvXyZaBCdEfGhIjKlMnOpQrStUvX" \ https://navi.gnexus.space/api/agents/profiles
Append the token as a query parameter:
const ws = new WebSocket(
`wss://navi.gnexus.space/ws/sessions/${sessionId}?api_token=${encodeURIComponent(token)}`
)
Security note: The query parameter is visible in server access logs. For production deployments with strict log retention policies, consider implementing a
{type: "auth", api_token: "..."}WebSocket message sent immediately after connect.
The web client stores the token in localStorage under key navi_api_token. When present, the WebSocket composable automatically appends ?api_token= on connect.
| Risk | Mitigation | Status |
|---|---|---|
| Token in URL query param → server logs | Documented; future: auth WS message | Accepted |
Token in localStorage → XSS exposure |
Single-origin policy; no third-party JS | Accepted |
| No rate limiting on token creation | Limited by requiring authenticated user | Accepted |
| No per-token scopes or expiration | MVP simplification; schema supports adding later | Accepted |
| Token hash collision | SHA-256 with 256-bit random input — negligible | Mitigated |
| Brute-force token guessing | 43-char URL-safe = ~256 bits; computationally infeasible | Mitigated |
The web client exposes token management at #settings (Settings → API Keys):