Skip to main content

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 access
  • read_only - read endpoints only

Read endpointsโ€‹

  • GET /admin/dashboard
  • GET /admin/keys
  • GET /admin/keys/usage
  • GET /admin/keys/{id}
  • GET /admin/logs
  • GET /admin/logs/stats
  • GET /admin/providers
  • GET /admin/plugins โ€” returns [{ "name": ..., "type": ..., "enabled": ... }]
  • GET /admin/health
  • GET /admin/config
  • GET /admin/config/history

Write endpoints (admin scope)โ€‹

  • POST /admin/keys
  • PUT /admin/keys/{id}
  • DELETE /admin/keys/{id}
  • POST /admin/keys/{id}/revoke
  • POST /admin/keys/{id}/rotate
  • DELETE /admin/logs
  • POST /admin/config
  • PUT /admin/config
  • DELETE /admin/config
  • POST /admin/config/rollback/{version}

Query parametersโ€‹

GET /admin/keys/usage:

  • limit (default 20, max 100)
  • offset (default 0)
  • sort (usage or last_used, default usage)
  • active (true or false)
  • since (RFC3339)

GET /admin/logs:

  • limit (default 50, max 200)
  • offset (default 0)
  • stage, model, provider
  • since (RFC3339)

GET /admin/logs/stats:

  • limit (top-N buckets, max 100)
  • stage, model, provider
  • since (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
}
warning

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"
}
  • name is required; an empty name returns 400.
  • scopes is optional โ€” when omitted it defaults to ["admin"].
  • expires_at is optional and must be RFC3339; an invalid value returns 400.

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}/revoke is a soft disable: it sets active = false and stamps revoked_at. The record is retained (still visible in GET /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/config returns the current full config as JSON.
  • PUT /admin/config replaces the running config (200, body { "status": "updated" }).
  • POST /admin/config also replaces it (201, body { "status": "created" }).
  • DELETE /admin/config resets 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.

danger

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 }
}
warning

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