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.

There is no OpenAPI document. Nothing in the tree generates one, so the endpoint index is the contract, and request and response bodies are documented only where they matter.

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 the convention SUPPORT.md uses:

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/jsonwriteJSON sets the header 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 501The monitor traffic command to run by hand.

Messages are written to be read by a person. Where the failure came from the router, the router's own words are passed through verbatim after a short prefix, so a 502 says what the router said rather than what the agent guessed.

Request body limits#

A cap is applied in front of the mux, before any handler reads a body, because a cap that each new handler has to remember is the same hole again.

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, which hijacks the connection and frames its own reads

The larger limit is spelled out per route rather than inferred, so adding a route does not silently widen it.

Timeouts#

SettingValueWhy
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 deliberately not cancelled when the browser tab closes — the caller must be able to find out whether it landed.

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 — sending HSTS from a plain-HTTP listener would be a promise the agent cannot keep.

Observation, logging and panics#

Every request is counted and logged before it is authenticated, which is why the metric label for a path the mux does not register collapses to /api/{unknown} rather than minting a 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"} rather than taking the router's management plane down.

What the API is not#

See also#

Checked against#

agent/main.go (runDaemon, routes, writeJSON, writeError, writeStatus), agent/security.go (securityHeaders, limitBodies, bodyLimit, observe, recoverPanics, routeLabel), agent/vyos.go (newVyosClient, clientFor, routerBudget), agent/metrics.go, docs/deploy.md, docs/security.md.

Updated 2026-09-02 api http