Wheelhouse docs

The HTTP API#

The agent serves one HTTP API and, optionally, the web UI from the same origin and the same port. Everything is JSON, everything below /api/ needs a principal except five routes, and every mutation needs the operator role and — for configuration changes — a licence. This page is what holds for every endpoint; the endpoint index is the list.

GET /api/openapi.json is the same list as OpenAPI 3.1, generated from the agent's own route table; wheelhouse-agent openapi prints it without needing a router, a key or a licence. CI fails when the generated description and the code disagree, so it is the contract and this page is prose around it.

Base URL#

InstallBase URL
Appliance (ISO or .deb)https://<router>:8443
Developer install (install/install.sh)http://127.0.0.1:8090, or https:// if you gave it a certificate

The shipped systemd unit passes --addr 0.0.0.0:8443 --tls-self-signed. The agent's default with no flags is 127.0.0.1:8090 and no TLS. See Ports and listeners.

Examples on this site use two shell variables:

bash
R=https://192.0.2.1:8443
T=wh_0123456789abcdef0123456789abcdef0123456789abcdef

curl -sk "$R/api/system" -H "Authorization: Bearer $T"

-k is there because the appliance's certificate is self-signed by default. Drop it once you have installed a real one.

Content types#

Responses are application/json on every path, including errors. Two exceptions:

EndpointContent type
GET /metricstext/plain; version=0.0.4; charset=utf-8
GET /api/streamA WebSocket upgrade, then JSON text frames

Request bodies are JSON everywhere the agent reads one. Handlers that treat a body as optional decode it and ignore the error, so a POST with no body gets the documented defaults rather than a 400 — POST /api/commit with no body commits without a confirm window, and POST /api/rollback with no body rolls back to revision 1.

The shape of an error#

Every error is a JSON object with one key:

json
{"error": "commit failed: Set failed"}

Four responses carry a second key beside error:

Extra keyOnMeaning
licenseEvery 402The licence state: unlicensed, expired, revoked, invalid, or the literal feature when the plan lacks a feature.
totp_requiredPOST /api/auth/login 401The password was right; a second factor is needed.
totp_enrolment_required403 on any route, and on a successful loginThe session may reach only the enrolment routes until a code proves the secret.
commandPOST /api/capture 501, and GET /api/capture when supported is falseThe monitor traffic command to run by hand.

Where the failure came from the router, the router's own words are passed through verbatim after a short prefix.

Request body limits#

A cap is applied before any handler reads a body.

LimitApplies to
1 MiBEvery route by default
16 MiBPOST /api/stage, POST /api/stage/remove, POST /api/configure, POST /api/config/load, POST /api/reconcile, POST /api/fleet/{id}/configure
noneGET /api/stream, whose inbound frames are capped at 4096 bytes each instead

Timeouts#

SettingValueNote
ReadHeaderTimeout10 s
ReadTimeout60 s
WriteTimeoutnoneThe telemetry stream and slow op-mode calls legitimately outlive any figure that would suit a normal request.
IdleTimeout120 s
Router read budget20 s per browser requestOne request may not spend longer than this talking to the router, however many reads it fans out to.

Behind that, the agent holds three HTTP clients for the router: 30 s for reads, 3 minutes for /configure and /config-file, and 15 minutes for image pulls, reboots and traceroute. A commit is not cancelled when the browser tab closes.

Transport and headers#

TLS is floored at 1.2 with an explicit suite list, and X25519 / P-256 / P-384 curve preferences; TLS 1.3 picks its own suites. Every response carries:

X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Cross-Origin-Opener-Policy: same-origin
Permissions-Policy: geolocation=(), microphone=(), camera=(), interest-cohort=()
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline';
  img-src 'self' data:; font-src 'self' data:; connect-src 'self' ws: wss:;
  frame-ancestors 'none'; base-uri 'none'; form-action 'self'

Strict-Transport-Security: max-age=31536000; includeSubDomains is added only when a certificate is configured.

Observation, logging and panics#

Every request is counted and logged before it is authenticated. A path the agent does not register is labelled /api/{unknown} rather than minting a metric series per request. One line per request:

level=INFO msg=request method=GET path=/api/interfaces status=200 bytes=8123 ms=2 ip=192.0.2.44

WARN at 4xx, ERROR at 5xx. A handler that panics is caught, logged with its stack, and answered with 500 {"error":"internal error — see the agent log"}.

What the API is not#

See also#

Updated 2026-09-02 api http