Skip to content

API tokens ​

Workspace-scoped CRUD over the per-workspace API tokens that non-admin clients (chat callers, integrations, the bw CLI) present on the /v1/* surface. These rows live in the tokens table with kind = "api_key"; the operator-level BIGWAVE_ADMIN_TOKEN is issued out-of-band and is not represented here.

The router lives at packages/shared/api_token_routes.py. Pydantic models are at packages/shared/api_tokens.py:73 (ApiTokenEntry), :88 (ApiTokenSecret), and :102 (ApiTokenCreateRequest).

Endpoints ​

MethodPathAuthSummary
GET/admin/api-tokensBearerList tokens for the caller's workspace.
POST/admin/api-tokensBearerMint a new token; raw secret is returned exactly once.
GET/admin/api-tokens/{token_id}BearerFetch one token entry; 404 cross-tenant.
POST/admin/api-tokens/{token_id}/revokeBearerMark a token revoked. Idempotent.

GET /admin/api-tokens ​

Returns {"tokens": ApiTokenEntry[]} — every non-secret field of each token in the caller's workspace. The raw secret is never returned by this route; once minted, only the hash lives server-side.

bash
curl -H "Authorization: Bearer $BIGWAVE_ADMIN_TOKEN" \
  http://localhost:8000/admin/api-tokens

POST /admin/api-tokens ​

Mint a new bw_…-prefixed token. Body ApiTokenCreateRequest:

json
{
  "name": "ci-runner",
  "user_id": "alice@example.com",
  "expires_at": "2026-12-31T00:00:00Z"
}

name is required (1–256 chars); user_id and expires_at are optional. Returns 201 with ApiTokenSecret:

json
{
  "entry": {
    "id": "tok_01HF…",
    "workspace_id": "default",
    "user_id": "alice@example.com",
    "name": "ci-runner",
    "kind": "api_key",
    "created_at": "2026-05-01T12:00:00Z",
    "expires_at": "2026-12-31T00:00:00Z",
    "revoked_at": null
  },
  "secret": "bw_AbCdEf…"
}

The raw secret is returned exactly once — store it immediately or rotate. The server only persists its hash.

bash
curl -X POST -H "Authorization: Bearer $BIGWAVE_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"ci-runner"}' \
  http://localhost:8000/admin/api-tokens

GET /admin/api-tokens/{token_id} ​

Returns the same ApiTokenEntry shape as the list route. Returns 404 when the id does not exist or belongs to a different workspace — absence and cross-tenant-existence collapse into the same response so workspace-id existence does not leak.

POST /admin/api-tokens/{token_id}/revoke ​

Sets revoked_at = now() on the row. Subsequent chat traffic authenticated with the raw secret is rejected as 401. Idempotent — calling it on an already-revoked token returns the same row.

bash
curl -X POST -H "Authorization: Bearer $BIGWAVE_ADMIN_TOKEN" \
  http://localhost:8000/admin/api-tokens/tok_01HF.../revoke

Operator notes ​

  • Workspace fence. Every route resolves the workspace from the bearer principal; the {token_id} path segment alone never identifies a token. See workspace fence.
  • Admin token vs API token. The bearer presented to these routes is the operator-level BIGWAVE_ADMIN_TOKEN (or the proxy-headers principal). The tokens managed here gate the OpenAI-shaped /v1/* surface for end-user clients.
  • Re-issuing. There is intentionally no rotation route. Operators rotate by minting a new token and revoking the old.
  • 503 causes. app.state.api_token_repo is wired in lifespan; routes return 503 with "api token repository not initialised" when the lifespan did not complete.
  • Audit. POST and revoke write audit_log rows when an AuditLogRepository is wired (see Audit).

Dual-licensed: MIT OR Apache-2.0