Admin API
Admin endpoints are mounted under /admin and protected with a bearer token.
Authenticate with the MASTER_KEY (the primary admin credential, generated by
ferrogw init) or an admin-scoped API key issued via POST /admin/keys.
Authorization: Bearer <master-key-or-admin-key>
Scopesโ
admin- full accessread_only- read endpoints only
Read endpointsโ
GET /admin/dashboardGET /admin/keysGET /admin/keys/usageGET /admin/keys/{id}GET /admin/logsGET /admin/logs/statsGET /admin/providersGET /admin/pluginsโ returns[{ "name": ..., "type": ..., "enabled": ... }]GET /admin/healthGET /admin/configGET /admin/config/history
Write endpoints (admin scope)โ
POST /admin/keysPUT /admin/keys/{id}DELETE /admin/keys/{id}POST /admin/keys/{id}/revokePOST /admin/keys/{id}/rotateDELETE /admin/logsPOST /admin/configPUT /admin/configDELETE /admin/configPOST /admin/config/rollback/{version}
Query parametersโ
GET /admin/keys/usage:
limit(default20, max100)offset(default0)sort(usageorlast_used, defaultusage)active(trueorfalse)since(RFC3339)
GET /admin/logs:
limit(default50, max200)offset(default0)stage,model,providersince(RFC3339)
GET /admin/logs/stats:
limit(top-N buckets, max100)stage,model,providersince(RFC3339)
DELETE /admin/logs:
- required:
before(RFC3339) - optional:
stage,model,provider
When request log storage is disabled, log endpoints return 501 not implemented.
API key schemaโ
Every key endpoint returns this JSON object. Timestamp fields are RFC3339 and
omitted when unset (omitempty).
{
"id": "1f2e3d4c-5b6a-7c8d-9e0f-a1b2c3d4e5f6",
"key": "fgw_3a7f...",
"name": "ci-pipeline",
"scopes": ["admin"],
"created_at": "2026-06-17T10:00:00Z",
"expires_at": "2026-12-31T23:59:59Z",
"revoked_at": null,
"rotated_at": null,
"last_used_at": "2026-06-17T11:42:00Z",
"usage_count": 128,
"active": true
}
The full fgw_... key string is returned only by POST /admin/keys (on
creation) and POST /admin/keys/{id}/rotate (on rotation). Every other endpoint
masks it to the first 8 characters plus ... (for example fgw_3a7f...). Store
the full value when it is first shown โ it cannot be retrieved again.
Create a key โ POST /admin/keysโ
Request:
{
"name": "ci-pipeline",
"scopes": ["admin"],
"expires_at": "2026-12-31T23:59:59Z"
}
nameis required; an empty name returns400.scopesis optional โ when omitted it defaults to["admin"].expires_atis optional and must be RFC3339; an invalid value returns400.
Response (201 Created) is the full API key schema with the complete, unmasked
key shown this one time.
Read keys โ GET /admin/keys and GET /admin/keys/{id}โ
GET /admin/keys returns a JSON array of key objects; GET /admin/keys/{id}
returns a single object (404 if the id is unknown). In both cases the key
field is masked to key[:8] + "...". There is no way to read a full key back
after creation/rotation.
Update a key โ PUT /admin/keys/{id}โ
Request (all fields optional):
{
"name": "renamed-key",
"scopes": ["read_only"],
"expires_at": "2027-01-01T00:00:00Z",
"clear_expiration": false
}
Set clear_expiration: true to remove an existing expiry (it takes precedence
over expires_at). The response is the updated, masked key object.
Rotate a key โ POST /admin/keys/{id}/rotateโ
Generates a brand-new fgw_... string for the same key id, sets rotated_at,
and invalidates the previous string immediately. The response is the full key
schema with the new unmasked key โ shown once, like creation.
Revoke vs deleteโ
POST /admin/keys/{id}/revokeis a soft disable: it setsactive = falseand stampsrevoked_at. The record is retained (still visible inGET /admin/keys) but can no longer authenticate. Response:{ "status": "revoked" }DELETE /admin/keys/{id}is a hard delete: the key record is removed from the store entirely. Response:204 No Content.
Both return 404 for an unknown id.
Key usage โ GET /admin/keys/usageโ
Returns a { data, summary, filters } envelope. data is an array of (masked)
key objects sorted by sort; summary aggregates the filtered set; filters
echoes the applied query parameters.
{
"data": [ /* masked API key objects */ ],
"summary": {
"total_keys": 12,
"active_keys": 9,
"total_usage": 4096,
"returned_keys": 12
},
"filters": {
"limit": 20,
"offset": 0,
"sort": "usage",
"active": "",
"since": ""
}
}
Request logs โ GET /admin/logsโ
Returns a { data, summary, filters } envelope where data holds request-log
entries:
{
"data": [ /* request log entries */ ],
"summary": {
"total_entries": 1500,
"returned_entries": 50
},
"filters": {
"limit": 50,
"offset": 0,
"stage": "",
"model": "",
"provider": "",
"since": ""
}
}
Log statistics โ GET /admin/logs/statsโ
Aggregates up to 5000 scanned entries into by_stage / by_provider /
by_model count maps. The limit query parameter trims the provider and model
maps to the top-N buckets. summary.truncated is true when more entries exist
than were scanned.
{
"summary": {
"total_entries": 5000,
"error_entries": 12,
"total_tokens": 1284000,
"truncated": true,
"available_entries": 7321,
"scan_limit": 5000
},
"by_stage": { "after_request": 4988, "on_error": 12 },
"by_provider": { "openai": 4200, "anthropic": 800 },
"by_model": { "gpt-4o": 3100, "claude-3-5-sonnet": 700 },
"filters": { "limit": 0, "stage": "", "model": "", "provider": "", "since": "" }
}
Installed plugins โ GET /admin/pluginsโ
Returns a JSON array describing the configured plugins:
[
{ "name": "word-filter", "type": "guardrail", "enabled": true },
{ "name": "max-token", "type": "guardrail", "enabled": false }
]
Gateway configurationโ
The config endpoints operate on the entire gateway config object โ every write is a full replace, not a partial patch. Supplying a partial body drops the omitted fields.
GET /admin/configreturns the current full config as JSON.PUT /admin/configreplaces the running config (200, body{ "status": "updated" }).POST /admin/configalso replaces it (201, body{ "status": "created" }).DELETE /admin/configresets to the startup config ({ "status": "deleted" }).
A rejected config returns 400 (invalid_config); a persistence failure returns
500. When config management is not wired in, these return 501.
GET /admin/config can echo literal secrets if they were written inline.
Prefer ${ENV} references in config values so the rendered config exposes the
variable name, not the credential.
Config history โ GET /admin/config/historyโ
Returns the in-memory version history accumulated by admin-API writes since the process started:
{
"data": [
{
"version": 1,
"updated_at": "2026-06-17T10:05:00Z",
"config": { /* full config snapshot */ },
"rolled_back_from": null
}
],
"summary": { "total_versions": 1 }
}
History is held in memory only and is lost on restart. It also covers only config changes made through the admin API since the current process started โ the file the gateway booted from is not recorded as version 0.
Roll back โ POST /admin/config/rollback/{version}โ
Re-applies the config snapshot identified by {version} and appends a new
history entry whose rolled_back_from records the version that was current
before the rollback. {version} must be a positive integer that exists in
history, otherwise 404.
{
"status": "rolled_back",
"rolled_back_to": 1,
"current_history_size": 3
}
Bootstrap keys (deprecated)โ
The legacy ADMIN_BOOTSTRAP_KEY / ADMIN_BOOTSTRAP_READ_ONLY_KEY env vars are
deprecated and emit deprecation warnings at startup. They are honored only on
first run while the API key store is empty and MASTER_KEY is unset. Prefer
the MASTER_KEY instead.
export ADMIN_BOOTSTRAP_KEY=change-me
export ADMIN_BOOTSTRAP_READ_ONLY_KEY=change-me
export ADMIN_BOOTSTRAP_ENABLED=true