Wheelhouse docs

Status codes#

The agent uses a small set of codes and uses them precisely, because on a management API the difference between "your credential is wrong", "your role is wrong" and "your licence is wrong" is three completely different afternoons. This page says which layer produces each one and what it means here rather than in general.

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

The whole set#

CodeProduced byMeans here
200every handlerSuccess. Note that 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.
401requireRole, handleLoginNot authenticated.
402requireLicense, requireFeatureAuthenticated and permitted, but there is no usable licence. Carries "license": <state>.
403requireRole, 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.
413the body limiterThe request body is over its cap.
429the login limitersToo many failed logins, from this address or against this account.
500recoverPanicsA handler panicked. The stack is in the agent log; the body says only internal error — see the agent log.
501handleCaptureStartPacket capture is not implemented. This is the only 501.
502handlersThe router did not answer, or answered with an error.
503handleOIDCLoginSingle 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, deliberately.
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 a failure to record as one.

402 — no usable licence#

json
{
  "error": "a licence is required to change configuration; enter one under Administration → 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. Losing sight of a router because a card expired would be a worse product than losing the ability to change it.

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. Deliberately not 401: the session is valid, the guess is not, and the web UI signs a user out on any 401 — which turned a typo in the current-password box into a trip to the login screen.

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": "packet capture is not implemented in this agent yet",
  "command": "monitor traffic interface eth1 filter 'port 53' save /tmp/capture.pcap"
}

The agent cannot drive monitor traffic and hand back a capture file, so it says so and gives you the command. It used to answer {"status":"started"} after running an unrelated read, which put a green "Capture started" on the Diagnostics page for a capture that never ran.

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

This is the code that matters most, because it 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

That last one is worth knowing about: the router reports a rejected command as HTTP 200 with an error field, not as an HTTP error. Handlers check both, which is why 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#

Checked against#

agent/main.go (writeError, writeStatus, handleCaptureStart, handleCommit, handleFleet), agent/authhttp.go (authenticate, requireRole, handleLogin, handleChangePassword, handleTOTPDisable), agent/license.go (requireLicense, requireFeature, licenseState.view), agent/admin.go, agent/apps.go (handleAppRestart), agent/vyos.go (decodeVyOS, routerError), agent/gaps.go (handleVRRP), agent/parity.go (handleIPsec, handleLLDP), agent/security.go (limitBodies, recoverPanics), agent/desired.go (reconciler.pass).

Updated 2026-09-02 api errors http