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#
| Install | Base 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:
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:
| Endpoint | Content type |
|---|---|
GET /metrics | text/plain; version=0.0.4; charset=utf-8 |
GET /api/stream | A 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:
{"error": "commit failed: Set failed"}Four responses carry a second key beside error:
| Extra key | On | Meaning |
|---|---|---|
license | Every 402 | The licence state: unlicensed, expired, revoked, invalid, or the literal feature when the plan lacks a feature. |
totp_required | POST /api/auth/login 401 | The password was right; a second factor is needed. |
totp_enrolment_required | 403 on any route, and on a successful login | The session may reach only the enrolment routes until a code proves the secret. |
command | POST /api/capture 501, and GET /api/capture when supported is false | The 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.
| Limit | Applies to |
|---|---|
| 1 MiB | Every route by default |
| 16 MiB | POST /api/stage, POST /api/stage/remove, POST /api/configure, POST /api/config/load, POST /api/reconcile, POST /api/fleet/{id}/configure |
| none | GET /api/stream, whose inbound frames are capped at 4096 bytes each instead |
Timeouts#
| Setting | Value | Note |
|---|---|---|
ReadHeaderTimeout | 10 s | |
ReadTimeout | 60 s | |
WriteTimeout | none | The telemetry stream and slow op-mode calls legitimately outlive any figure that would suit a normal request. |
IdleTimeout | 120 s | |
| Router read budget | 20 s per browser request | One 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.44WARN 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#
- Authenticating — the two credentials and the CSRF rule.
- Endpoint index — every route.
- Role matrix — who may call what.
- Status codes — what each code means here.
- What a read hides
- The telemetry stream
- Prometheus metrics
- The agent and the router — what sits where.
- Advanced mode and the router's commands