Use this page to look up control-plane endpoints, roles, request fields, and response semantics.
Open These First
Use this table when you need the fastest runtime-introspection path:
| Need | Endpoint |
|---|---|
| current runtime state, backend health, quota backend state, watchdog state, observability package metadata | GET /admin/runtime |
| retained generations, rollback candidates, and recent runtime operations | GET /admin/runtime/history |
| one generation's retained record and related entries | GET /admin/runtime/history/{generation} |
| check whether a candidate config is valid and compatible | POST /admin/runtime/validate |
| dry-run a change and record it in history | POST /admin/runtime/preview |
| commit a compatible runtime-managed change | POST /admin/runtime/activate |
Endpoint Index
| Endpoint | Method | Minimum role | Purpose |
|---|---|---|---|
/health |
GET |
none | liveness |
/ready |
GET |
none | readiness |
/admin/runtime |
GET |
viewer |
current runtime snapshot |
/admin/runtime/history |
GET |
viewer |
retained generations and operation history |
/admin/runtime/history/{generation} |
GET |
viewer |
one retained generation and related entries |
/admin/runtime/validate |
POST |
operator |
validate a candidate without mutating runtime |
/admin/runtime/preview |
POST |
operator |
validate and record a preview |
/admin/runtime/activate |
POST |
operator |
activate a compatible candidate generation |
/admin/runtime/rollback |
POST |
operator |
restore a retained generation |
/admin/runtime/reload |
POST |
operator |
legacy full-config reload shortcut |
/admin/runtime/reload-certs |
POST |
operator |
reload listener certificate material |
/admin/runtime/restart |
POST |
admin |
request controlled restart through the watchdog |
Common Control API Flows
Read Current Runtime State
curl -k --http1.1 \
-H "Authorization: Bearer <token>" \
https://127.0.0.1:9902/admin/runtime
Validate, Preview, and Activate a Candidate
curl -k --http1.1 -X POST \
-H "Authorization: Bearer <token>" \
-H "content-type: application/json" \
https://127.0.0.1:9902/admin/runtime/validate \
-d '{"config_path":"/etc/spooky/candidate.yaml","requested_by":"ops","reason":"preflight"}'
curl -k --http1.1 -X POST \
-H "Authorization: Bearer <token>" \
-H "content-type: application/json" \
https://127.0.0.1:9902/admin/runtime/preview \
-d '{"config_path":"/etc/spooky/candidate.yaml","requested_by":"ops","reason":"preview"}'
curl -k --http1.1 -X POST \
-H "Authorization: Bearer <token>" \
-H "content-type: application/json" \
https://127.0.0.1:9902/admin/runtime/activate \
-d '{"config_path":"/etc/spooky/candidate.yaml","expected_generation":12,"requested_by":"ops","reason":"deploy"}'
Roll Back to a Retained Generation
curl -k --http1.1 \
-H "Authorization: Bearer <token>" \
https://127.0.0.1:9902/admin/runtime/history
curl -k --http1.1 -X POST \
-H "Authorization: Bearer <token>" \
-H "content-type: application/json" \
https://127.0.0.1:9902/admin/runtime/rollback \
-d '{"target_generation":11,"expected_active_generation":12,"requested_by":"ops","reason":"rollback"}'
Protocol
The Control API uses HTTP/1.1 over TLS. HTTP/2 is not supported.
When using curl, pass --http1.1 explicitly — curl negotiates h2 by default when connecting to a TLS endpoint and the server will reject the connection:
curl -k --http1.1 https://<address>:<port>/...
The -k flag skips certificate verification for self-signed certs.
Authentication
Supported authentication shapes:
- bearer token only
- mTLS only
- mTLS + bearer token
Authentication and authorization are separate concerns:
- authentication proves who the caller is
- authorization decides whether the caller has
viewer,operator, oradmin
Bearer-token form:
Authorization: Bearer <token>
Compatibility note:
observability.control_api.auth_tokenremains supported as the legacy single-token admin credential- the legacy token is mapped internally to a static admin identity so existing operators keep current restart/reload privileges during migration
- new deployments should prefer
observability.control_api.auth.bearer_tokens[]with explicit roles - compatibility boundary: a new Spooky binary accepts legacy
auth_tokenconfigs, but an older binary will reject configs that use the newer nested control-plane fields becauseControlApiuses strictdeny_unknown_fields
Role model:
viewer: runtime snapshot and history readsoperator:viewerplus validate, preview, activate, rollback, reload, and cert reloadadmin:operatorplus restart and future destructive admin actions
Route Access Rules
Route families:
/healthand/ready: unauthenticated or separately protected/admin/runtime*reads:viewer- runtime mutation routes except restart:
operator /admin/runtime/restart:admin
Contract rules:
vieweris the minimum privileged read roleoperatoris the minimum non-restart mutation roleadminis required for restart- implementation should distinguish invalid authentication from insufficient role
Response Contract
Privileged routes distinguish authentication failure from authorization failure:
401 Unauthorized: missing authentication or invalid authentication403 Forbidden: authenticated caller is under-scoped, or the source-address policy rejected the request
Representative reasons returned in JSON payloads:
missing_authenticationinvalid_bearer_tokeninsufficient_rolesource_ip_not_allowed
When control API mTLS is configured as required, missing or invalid client certificates are rejected during the TLS handshake before HTTP routing. That failure does not produce an HTTP 401 or 403 response.
Configuration Patterns
Bearer-Only Local Dev
Use this for loopback-only development or local automation.
observability:
control_api:
enabled: true
address: "127.0.0.1"
port: 9890
auth_token: "change-me-local-dev"
mTLS Optional With Viewer Token
Use this when you want to accept client certificates without making them mandatory yet.
observability:
control_api:
enabled: true
address: "127.0.0.1"
port: 9902
tls:
client_auth:
mode: optional
ca_file: "/etc/spooky/pki/admin-ca.pem"
auth:
bearer_tokens:
- token: "viewer-token"
role: viewer
actor_id: "ops-readonly"
mTLS Required With Operator/Admin Identities
Use this for hardened production admin-plane access.
observability:
control_api:
enabled: true
required: true
address: "10.0.10.5"
port: 9902
tls:
client_auth:
mode: required
ca_file: "/etc/spooky/pki/admin-ca.pem"
auth:
bearer_tokens:
- token: "operator-token"
role: operator
actor_id: "ops-automation"
- token: "admin-token"
role: admin
actor_id: "platform-admin"
identity_source:
kind: "mtls_subject_cn"
role_attribute: "OU"
ip_allowlist:
cidrs:
- "10.0.10.0/24"
audit:
enabled: true
format: json
sink: log
Endpoints
GET /health
Purpose:
- liveness check
- watchdog state visibility
Expected use:
- load balancer or platform liveness probe
- operator sanity check
GET /ready
Purpose:
- readiness state for serving traffic
Expected use:
- deployment orchestration
- maintenance and rollout checks
GET /admin/runtime
Purpose:
- runtime snapshot for operators
Minimum role:
viewer
Typical contents include:
- worker and runtime state
- key counters
- admission state
- backend health summary
- quota backend health summary
- observability package metadata
- recent admin actions when available
- dashboard and documentation references for the shipped operator bundle
Expected use:
- debugging
- rollout validation
- incident response
The observability block is the packaged runtime-introspection entry point for operators. The high-signal fields are:
contract_versionaudit_schema_versioncurrent_generationdashboard_packagesdocumentationbackend_health_summaryquota_backend_health_summaryrecent_admin_actions
Example:
{
"generation": 12,
"readiness": "ready",
"observability": {
"contract_version": "v1",
"audit_schema_version": "v1",
"current_generation": 12,
"backend_health_summary": {
"healthy": 7,
"unhealthy": 1
},
"quota_backend_health_summary": {
"backend_mode": "redis",
"availability": "available"
}
}
}
POST /admin/runtime/validate
Purpose:
- parse and validate a candidate config, and report whether it could be activated — without touching the running runtime
Returns 200 with a plan describing the candidate generation, a per-domain diff, and any rejected changes. A config that cannot be activated still returns 200; inspect rejected_changes and candidate_status rather than relying on the status code.
Accepts the same optional body fields as /admin/runtime/reload.
Expected use:
- CI gating on config changes before a deploy
- confirming a config is loadable before scheduling a maintenance window
Minimum role:
operator
Example:
curl -k --http1.1 -X POST https://127.0.0.1:9902/admin/runtime/validate \
-H "Authorization: Bearer <token>" \
-H "content-type: application/json" \
-d '{"config_path":"/etc/spooky/candidate.yaml","requested_by":"ops","reason":"preflight"}'
POST /admin/runtime/preview
Purpose:
- same planning work as validate, recorded in generation history as an operator preview
Returns 200 with the same plan shape as validate. Neither endpoint mutates the active generation.
Expected use:
- operator dry-run immediately before an activation, when you want the attempt in the audit trail
Minimum role:
operator
POST /admin/runtime/activate
Purpose:
- stage and commit a config change, returning the structured activation result
Returns 202 on success. Failures are classified rather than collapsed into 500:
| Status | Meaning |
|---|---|
400 |
the candidate config is invalid |
409 |
conflict — a stale expected_generation, or changes that require a restart |
500 |
resource preparation failed, or the runtime swap itself failed |
Accepts the same optional body fields as /admin/runtime/reload.
Expected use:
- the preferred activation path — prefer this over the legacy
/reloadshortcut, since it returns the full diff, rejection detail, and generation history entry
Minimum role:
operator
Example:
curl -k --http1.1 -X POST https://127.0.0.1:9902/admin/runtime/activate \
-H "Authorization: Bearer <token>" \
-H "content-type: application/json" \
-d '{"config_path":"/etc/spooky/candidate.yaml","expected_generation":12,"requested_by":"ops","reason":"deploy"}'
POST /admin/runtime/rollback
Purpose:
- restore a previously retained runtime generation
Required request body:
| Field | Type | Purpose |
|---|---|---|
target_generation |
integer | The retained generation to restore. Required. |
expected_active_generation |
integer | Reject with 409 unless this matches the active generation. |
requested_by |
string | Recorded in generation history for audit. |
reason |
string | Recorded in generation history for audit. |
Returns 202 on success. Failures:
| Status | Meaning |
|---|---|
404 |
the target generation is not retained (unknown generation) |
409 |
the target is retained but not rollback-eligible, or the active generation moved |
500 |
resource preparation failed, or the rollback swap itself failed |
Use GET /admin/runtime/history first to pick a target whose rollback_candidate is true.
Minimum role:
operator
Example:
curl -k --http1.1 -X POST https://127.0.0.1:9902/admin/runtime/rollback \
-H "Authorization: Bearer <token>" \
-H "content-type: application/json" \
-d '{"target_generation": 3}'
GET /admin/runtime/history
Purpose:
- list retained runtime generations and the recorded history of control-plane operations
Response shape:
| Field | Type | Purpose |
|---|---|---|
active_generation |
integer | The generation currently serving traffic. |
retained_generations |
array | Retained generation records — the state needed to choose a rollback target. |
entries |
array | Operation log (validate, preview, activate, rollback), newest first. |
Each entry in retained_generations:
| Field | Type | Purpose |
|---|---|---|
generation |
integer | The generation number. |
status |
string | One of active, previous, failed_prepare, rolled_back, superseded. |
rollback_candidate |
bool | Whether /admin/runtime/rollback will accept this generation as a target. |
has_bundle |
bool | Whether the runtime bundle is still retained. A rollback target needs true. |
note |
string | Present only when there is explanatory detail, e.g. why a staged prepare failed. |
status: failed_prepare records a candidate generation that was never successfully prepared; it has has_bundle: false and carries the failure reason in note.
Expected use:
- choosing a safe rollback target
- auditing who changed runtime config, when, and from which config source
- diagnosing why a staged activation never committed
- correlating runtime operations with audit and observability views
Minimum role:
viewer
Example:
curl -k --http1.1 \
-H "Authorization: Bearer <token>" \
https://127.0.0.1:9902/admin/runtime/history
GET /admin/runtime/history/{generation}
Purpose:
- the retained-generation record and operation entries for a single generation
Returns 200 with generation, a single retained_generation object (same shape as above), and the entries recorded against it. Returns 404 if that generation is not retained.
Minimum role:
viewer
POST /admin/runtime/reload
Legacy shortcut. Prefer POST /admin/runtime/activate, which returns the full diff and rejection detail.
Purpose:
- reload the full config from disk and apply changes to upstreams, backends, policies, timeouts, and
log.level
Config source:
- with no request body, the reload re-reads the currently active runtime config source
- on a freshly started process that source is the path passed at startup, but activating an alternate
config_pathmakes that file the active source for every later reload - pass
config_pathin the body to read a different file; a successful activation makes that path the new active source
Important scope note:
- listener bind addresses, control API bind, and metrics bind cannot change without a restart
- log format/file settings, tracing config (
observability.tracing.*), andperformance.control_plane_threadsalso require a restart (a reload changing them is rejected);log.level, however, is applied live - in-flight requests on the old config complete normally; new requests use the new config immediately
Expected use:
- adding or removing backends
- changing load balancing, timeouts, resilience, or routing policy at runtime
Minimum role:
operator
Optional request body:
| Field | Type | Purpose |
|---|---|---|
config_path |
string | Read this config file instead of the active source. On success it becomes the new active source. |
expected_generation |
integer | Reject with 409 unless this matches the active generation (optimistic concurrency). |
requested_by |
string | Recorded in generation history for audit. |
reason |
string | Recorded in generation history for audit. |
Example:
curl -k --http1.1 -X POST https://127.0.0.1:9902/admin/runtime/reload \
-H "Authorization: Bearer <token>"
Activating an alternate config file:
curl -k --http1.1 -X POST https://127.0.0.1:9902/admin/runtime/reload \
-H "Authorization: Bearer <token>" \
-H "content-type: application/json" \
-d '{"config_path": "/etc/spooky/canary.yaml"}'
POST /admin/runtime/reload-certs
Purpose:
- reload listener certificate and related trust material for new handshakes
Important scope note:
- this is not full config hot reload
- existing sessions keep their already-negotiated certificate and auth state
Expected use:
- listener certificate rotation
- listener trust-material refresh
Minimum role:
operator
POST /admin/runtime/restart
Purpose:
- request a controlled restart/drain workflow through the watchdog coordinator
Expected use:
- operational restart requests
- orchestrated maintenance flow
Minimum role:
admin
Audit Configuration And Event Shape
The control API audit stream is the operator history surface for admin-plane actions.
Example:
observability:
control_api:
audit:
enabled: true
format: json
sink: log
Current audit schema version:
v1
The stable top-level event fields are:
schema_versionevent_idevent_typetime_unix_msrequest_idtrace_idspan_idlisteneractoractiontargetgenerationresultreasonfailure_classpeer_addrauthn
Use audit for actor attribution, authn and authz failure history, and attempt-versus-result correlation for runtime operations.