Wheelhouse docs

Status codes#

What each code means on this API, and which layer produced it. 401, 402 and 403 are three different problems.

Every failure body is {"error": "…"}, sometimes with one extra key. See The HTTP API.

The whole set#

CodeProduced byMeans here
200every handlerSuccess. A router error can still arrive inside a 200 body on some reads — see below.
101the WebSocket upgradeOnly on GET /api/stream, and only as a metrics label; the client sees a normal upgrade.
400handlersThe request is wrong: bad JSON, a missing required field, an operation that is not set or delete, an empty element in a path, a rev that is not a number, a host that is not a host, a desired-state document with no keys, a file name outside the data directory, an image reference that is not one.
401the authentication check, and the login routeNot authenticated.
402the licence gateAuthenticated and permitted, but there is no usable licence. Carries "license": <state>.
403the role check, and three handlersAuthenticated, but not allowed.
404handlersNo such user, token, session, catalogue entry, installed app, or fleet router.
409handlersA conflict with the current state: the name exists, the last admin, your own account, an app that is already stopped, an identity already linked elsewhere. On POST /api/capture, a capture is already running on <iface> — stop it first. On POST /api/capture/stop, the capture had already ended. On GET /api/capture/download, the capture is still running.
413the body limiterThe request body is over its cap.
429the login limitersToo many failed logins, from this address or against this account.
500a handler that panickedThe stack is in the agent log; the body says only internal error — see the agent log.
501POST /api/captureOnly when the agent is not on the router, or the router has no tcpdump; the body carries the command to run by hand. This is the only 501.
502handlersThe router did not answer, or answered with an error.
503GET /api/oidc/loginSingle sign-on is not configured on this agent.

401 — not authenticated#

Five messages, and they mean different things:

MessageCause
unauthorizedNo cookie and no Authorization header.
session expiredThe cookie names a session the agent does not hold, and no Bearer token was sent either.
invalid tokenAn Authorization: Bearer value that matches no stored token and is not the break-glass token. An expired token is indistinguishable from an unknown one.
missing or invalid CSRF tokenA cookie-authenticated mutation with no X-Wheelhouse-CSRF header, or the wrong value.
invalid credentialsPOST /api/auth/login. Never distinguishes "no such user" from "wrong password".

POST /api/auth/login also answers 401 with {"error":"two-factor code required", "totp_required":true} when the password was right and a code is needed. That is not an authentication failure.

402 — no usable licence#

json
{
  "error": "a licence is required to change configuration; enter one under Admin → Licence",
  "license": "unlicensed"
}

The message is replaced by the licence's own explanation when it has one — for example the licence expired on 2026-08-01 — configuration changes are disabled. The license field is the state:

licenseMeaning
unlicensedNo key is held.
expiredThe key is past expiry and past its grace period.
revokedThe licence server said so. The token may still verify; the entitlement is gone.
invalidThe key on file does not parse, does not verify, or was signed by a key this build does not know.
featureThere is a usable licence, but its plan does not carry the feature this route needs — today, only fleet.

Every write plane route answers 402 without a licence. No read does, and neither does the admin plane: an unlicensed router keeps every page, every read endpoint, the audit log, sign-in, and account and licence management.

The reconcile loop obeys the same gate: without a licence it records no usable licence: the reconcile loop is paused until one is entered in GET /api/drift and changes nothing.

403 — not allowed#

MessageCause
this action requires the <role> roleThe route's minimum role is above the principal's. Recorded in the audit log as denied <METHOD> <path>.
two-factor is required by policy; finish enrolment before using this accountAn enrolling session outside the four enrolment routes. Carries totp_enrolment_required: true.
two-factor is required by policy and cannot be disabledPOST /api/auth/totp/disable while require_totp is on.
current password is incorrectPOST /api/auth/password. Not 401: the session is valid, the guess is not, and the web UI signs a user out on any 401.

409 — conflict#

MessageRoute
user "x" already existsPOST /api/admin/users
this is the only admin — promote another before changing this onePATCH /api/admin/users/{name}
this is the only admin — create another before deleting itDELETE /api/admin/users/{name}
you cannot delete the account you are signed in asDELETE /api/admin/users/{name}
that identity is already linked to "x"PATCH /api/admin/users/{name} with an oidc_subject another account claims
"x" is stopped — start it instead of restarting itPOST /api/apps/restart

501 — not implemented#

There is exactly one:

bash
curl -sk -X POST "$R/api/capture" -H "Authorization: Bearer $T" \
  -H 'Content-Type: application/json' -d '{"interface":"eth1","filter":"port 53"}'
json
{
  "error": "This agent runs off-router, so there is no interface here to capture from. Run the agent on the router, or run the command below over SSH.",
  "command": "monitor traffic interface eth1 filter 'port 53' save /config/capture.pcap"
}

The other 501 reads tcpdump is not installed at /usr/bin/tcpdump on this router. In both cases the command field is the command to run on the router yourself. On a normal install neither applies and the capture runs.

502 — the router did not answer, or said no#

A 502 means the failure is one layer further down. Three distinct situations arrive as 502:

SituationWhat the message looks like
The agent could not reach the routerPOST /show: dial tcp 127.0.0.1:443: connect: connection refused
The router's API answered with an HTTP errorthe router's API answered HTTP 404: Not Found (is service https api rest configured?)
The router answered 200 and reported a failure of its owncommit failed: Set failed — the router's own words, verbatim

The router reports a rejected command as HTTP 200 with an error field, not as an HTTP error. The agent checks both, so a commit the router refused is a 502 carrying the router's message rather than a 200 with success: true.

If every call 502s, check --api-url, the API key, and that the router's REST endpoints are enabled — with only a key configured and no service https api rest, the router's API answers 404 to everything. See What first boot does.

What is not an error#

Several reads answer 200 with a "not configured" shape rather than failing, because an unset subtree is the normal state of a router that does not use that feature:

EndpointQuiet answer
GET /api/vrrprunning: false, empty groups. The router answers a read of an empty path with HTTP 400 Configuration under specified path is empty, which the handler treats as "not configured", not as an error.
GET /api/vpn/ipsecconfigured: false, running: false. IPsec not initialized is a state, not a fault.
GET /api/lldpconfigured: false when the router says the service is not configured.
GET /api/routingbgp and ospf each not-configured.
GET /api/daemonsEach of upnp, dns, dhcp as configured or not-configured.
GET /api/appsavailable: false with the error text when the container subsystem cannot be read at all.
GET /api/fleetenabled: false with a note when fleet mode is off or the licence lacks the feature. This route never 402s.

See also#

Updated 2026-09-02 api errors http