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#
| Code | Produced by | Means here |
|---|---|---|
| 200 | every handler | Success. Note that a router error can still arrive inside a 200 body on some reads — see below. |
| 101 | the WebSocket upgrade | Only on GET /api/stream, and only as a metrics label; the client sees a normal upgrade. |
| 400 | handlers | The 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. |
| 401 | requireRole, handleLogin | Not authenticated. |
| 402 | requireLicense, requireFeature | Authenticated and permitted, but there is no usable licence. Carries "license": <state>. |
| 403 | requireRole, and three handlers | Authenticated, but not allowed. |
| 404 | handlers | No such user, token, session, catalogue entry, installed app, or fleet router. |
| 409 | handlers | A 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. |
| 413 | the body limiter | The request body is over its cap. |
| 429 | the login limiters | Too many failed logins, from this address or against this account. |
| 500 | recoverPanics | A handler panicked. The stack is in the agent log; the body says only internal error — see the agent log. |
| 501 | handleCaptureStart | Packet capture is not implemented. This is the only 501. |
| 502 | handlers | The router did not answer, or answered with an error. |
| 503 | handleOIDCLogin | Single sign-on is not configured on this agent. |
401 — not authenticated#
Five messages, and they mean different things:
| Message | Cause |
|---|---|
unauthorized | No cookie and no Authorization header. |
session expired | The cookie names a session the agent does not hold, and no Bearer token was sent either. |
invalid token | An 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 token | A cookie-authenticated mutation with no X-Wheelhouse-CSRF header, or the wrong value. |
invalid credentials | POST /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#
{
"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:
license | Meaning |
|---|---|
unlicensed | No key is held. |
expired | The key is past expiry and past its grace period. |
revoked | The licence server said so. The token may still verify; the entitlement is gone. |
invalid | The key on file does not parse, does not verify, or was signed by a key this build does not know. |
feature | There 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#
| Message | Cause |
|---|---|
this action requires the <role> role | The 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 account | An enrolling session outside the four enrolment routes. Carries totp_enrolment_required: true. |
two-factor is required by policy and cannot be disabled | POST /api/auth/totp/disable while require_totp is on. |
current password is incorrect | POST /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#
| Message | Route |
|---|---|
user "x" already exists | POST /api/admin/users |
this is the only admin — promote another before changing this one | PATCH /api/admin/users/{name} |
this is the only admin — create another before deleting it | DELETE /api/admin/users/{name} |
you cannot delete the account you are signed in as | DELETE /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 it | POST /api/apps/restart |
501 — not implemented#
There is exactly one:
curl -sk -X POST "$R/api/capture" -H "Authorization: Bearer $T" \
-H 'Content-Type: application/json' -d '{"interface":"eth1","filter":"port 53"}'{
"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:
| Situation | What the message looks like |
|---|---|
| The agent could not reach the router | POST /show: dial tcp 127.0.0.1:443: connect: connection refused |
| The router's API answered with an HTTP error | the router's API answered HTTP 404: Not Found (is service https api rest configured?) |
| The router answered 200 and reported a failure of its own | commit 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:
| Endpoint | Quiet answer |
|---|---|
GET /api/vrrp | running: 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/ipsec | configured: false, running: false. IPsec not initialized is a state, not a fault. |
GET /api/lldp | configured: false when the router says the service is not configured. |
GET /api/routing | bgp and ospf each not-configured. |
GET /api/daemons | Each of upnp, dns, dhcp as configured or not-configured. |
GET /api/apps | available: false with the error text when the container subsystem cannot be read at all. |
GET /api/fleet | enabled: false with a note when fleet mode is off or the licence lacks the feature. This route never 402s. |
See also#
- Endpoint index
- Role matrix — 401, 402 and 403 are three different layers.
- Authenticating
- Licence keys and states — where the
licensefield comes from. - Exit codes — the command-line equivalent.
- Troubleshooting
- The licence gate — why 402 exists.
- If it goes wrong
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).