Admin Console — API Keys¶
CRUD operations for the API keys that gate /v1/*. All endpoints
require an authenticated session (and CSRF on writes).
Key object¶
{
"id": 1,
"key": "deadbeef...", // opaque string, treat as a secret
"name": "claude-code-laptop", // human label, ≤ 120 chars
"is_active": true, // bool; inactive keys are 401'd at the proxy
"created_at": "2026-04-25T07:14:33+00:00",
"last_used_at": "2026-04-26T01:02:11+00:00", // null if never used; coalesced ~1/min
"total_request_count": 1234,
"total_input_tokens": 9876543,
"total_output_tokens": 1234567
}
last_used_at is updated at most once per minute per key (write
coalescing). Token totals are incremented on each response close, so
they reflect successful billable requests only.
GET /api/keys¶
List all keys.
| Auth | Session |
| CSRF | Not required (read) |
Returns [Key, ...] ordered by id ascending.
POST /api/keys¶
Mint a new key.
| Auth | Session |
| CSRF | Required |
Request¶
{
"name": "claude-code-laptop", // required, 1..120 chars
"key": "<optional explicit key>" // optional; defaults to uuid4().hex
}
Responses¶
| Status | Body |
|---|---|
| 201 | full Key object |
| 400 | {"error": "name is required"} / name must be a string / name must not be empty / name must be at most 120 characters / key must be a non-empty string |
| 409 | {"error": "Key already exists"} (if explicit key collides) |
When key is omitted, the server generates a random hex string via
uuid.uuid4().hex (32 chars, ~128 bits of entropy). It is returned
once in the response body; the server never returns it again —
the admin UI shows a copy-button on key creation specifically for that
reason.
PATCH /api/keys/<id>¶
Update a key's name and/or is_active.
| Auth | Session |
| CSRF | Required |
Request¶
At least one field must be present:
{
"name": "renamed", // optional, same validation as POST
"is_active": false // optional, must be boolean
}
Responses¶
| Status | Body |
|---|---|
| 200 | full Key object (post-update) |
| 400 | validation error / "No fields to update" |
| 404 | {"error": "Key not found"} |
Setting is_active: false immediately invalidates the key on
subsequent /v1/* requests — there is no client-side
reconnection grace.
DELETE /api/keys/<id>¶
Hard-delete the key row. Usage history attached to the key (in
daily_usage and usage_buckets) is not cascaded by application
code; the SQLite schema does not declare ON DELETE CASCADE on those
tables.
| Auth | Session |
| CSRF | Required |
Responses¶
| Status | Body |
|---|---|
| 200 | {"ok": true} |
| 404 | {"error": "Key not found"} |
Operational notes¶
- Keys are stored verbatim in SQLite at
proxy.db(no hash). The database is only readable by theglm-proxyservice user; full-disk encryption + EBS-volume isolation are the layer protecting them. - Deactivating a key is preferred to deleting it — the row keeps the historical totals visible for billing / audit.
- The admin UI surfaces totals from
total_input_tokens/total_output_tokensdirectly; for time-bucketed views see Usage Statistics.