67 docs indexed
How to mint, list, disable, and delete tenant API keys, and how obleth stores them securely.
API keys are the credentials clients use to authenticate with the obleth data plane. Every key belongs to exactly one tenant and inherits that tenant's weight, quota, and fairshare group. Clients can also authenticate with identity tokens from an OIDC provider — see Identity Tokens.
obleth never stores the raw secret. When a key is created:
sk_ + 48 random hex characters, from 24 random bytes).sha256(secret) is stored in Postgres (api_keys.key_hash).sk_a1b2c3d4e5f6a1b — is stored for dashboards.ResolvedKey (tenant details + weight + quota) is synced to Redis at obleth:key:{hash}.The raw secret is returned once in the creation response. If you lose it, the only option is to delete the key and create a new one.
Set OBLETH_API_KEY_PEPPER to mix a server-side secret into every key hash.
Unlike a per-key salt, the pepper is held outside the database, so a leak of the
config database yields hashes that cannot be checked against guessed keys
without also stealing the pepper. Hashing becomes
sha256(secret ‖ 0x00 ‖ pepper).
Leave it unset and hashing is byte-for-byte identical to the unpeppered scheme, so existing keys keep working. Adding, changing, or removing a pepper invalidates every key already issued — they must be rotated. Keep it secret and stable, and set the same value on every gateway replica.
From the dashboard, click New key on the API Keys page to mint a key for any tenant. You can set an optional name, description, and per-key token/cost budget; the raw secret is shown once on the success screen.
Or create one via the Management API:
curl -X POST http://localhost:9180/api/v1/tenants/$TID/keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "prod"}'
Response:
{
"key": {
"id": "...",
"tenant_id": "...",
"name": "prod",
"key_prefix": "sk_a1b2c3d4e5f6a1b",
"disabled": false,
"created_at": "2026-01-15T10:00:00Z"
},
"secret": "sk_a1b2c3d4e5f6..."
}
A key can carry its own cumulative budget, independent of the tenant's. Set it
at creation or with PUT /api/v1/keys/{id}:
curl -X POST http://localhost:9180/api/v1/tenants/$TID/keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "ci",
"budget_tokens": 5000000,
"budget_cost_usd": 25.0,
"budget_period": "monthly"
}'
Either cap (or both) may be set; budget_period is lifetime, monthly, or
term. When the key's own budget is exhausted the data plane returns
403 api key term budget exhausted — distinct from the tenant-level
403 tenant term budget exhausted. Passing null for a cap clears it. See
Quotas & Rate Limits.
# All keys for a specific tenant
curl http://localhost:9180/api/v1/tenants/$TID/keys \
-H "Authorization: Bearer $TOKEN"
# All keys across all tenants (with optional filters)
curl "http://localhost:9180/api/v1/keys?limit=50" \
-H "Authorization: Bearer $TOKEN"
To see when a key was last used, what it last called, and its recent volume, query the per-key usage summary. This is computed in ClickHouse, so you never have to page through the request log to find a key's last activity:
curl "http://localhost:9180/api/v1/keys/$KEY_ID/usage" \
-H "Authorization: Bearer $TOKEN"
# -> { "last_used_ms": 1749427821000, "last_model": "gemma4-31b-it",
# "last_status_code": 200, "requests": 142, "total_tokens": 99800, "cost_usd": 1.24 }
last_used_ms is 0 for a key that has never been used (within the ledger's
retention window). The requests / *_tokens / cost_usd totals cover a
rolling window (since_ms, default last 24h). For a fleet-wide view — e.g. to
find stale keys — use the bulk form GET /api/v1/usage/keys/summary, which the
dashboard Keys page uses for its Last used column. See the
Management API reference.
Disabling a key immediately prevents it from authenticating without deleting the key's audit history:
curl -X PUT http://localhost:9180/api/v1/keys/$KEY_ID/disabled \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"disabled": true}'
The change is synced to Redis and invalidated in moka across all pods. The key returns 403 api key disabled immediately on the next request.
Re-enable:
curl -X PUT http://localhost:9180/api/v1/keys/$KEY_ID/disabled \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"disabled": false}'
curl -X DELETE http://localhost:9180/api/v1/keys/$KEY_ID \
-H "Authorization: Bearer $TOKEN"
This removes the key from Postgres and Redis. The audit log retains the deletion event. Any client using the deleted key will receive 401 invalid api key.
In the dashboard, deleting a single key opens a confirmation dialog stating the
consequence. Selecting several keys deletes them as a batch, and Delete all
filtered keys — which deletes every key matching the active filters, not just
the visible page — additionally requires typing DELETE and ticking an
acknowledgement.
To rotate a key with zero downtime:
POST /api/v1/tenants/$TID/keysDELETE /api/v1/keys/$OLD_KEY_IDDon't use disable+enable for rotation — just create a new one and delete the old.
A tenant can have many keys. All keys for the same tenant share the same weight and quota. Useful patterns:
prod, staging, cichatbot, search, summarizerUsage in ClickHouse is recorded per key_id, so you can break down cost by key even within the same tenant.