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#
| Code | Produced by | Means here |
|---|---|---|
| 200 | every handler | Success. 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 | the authentication check, and the login route | Not authenticated. |
| 402 | the licence gate | Authenticated and permitted, but there is no usable licence. Carries "license": <state>. |
| 403 | the role check, 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. 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. |
| 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 | a handler that panicked | The stack is in the agent log; the body says only internal error — see the agent log. |
| 501 | POST /api/capture | Only 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. |
| 502 | handlers | The router did not answer, or answered with an error. |
| 503 | GET /api/oidc/login | 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. |
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 an
authentication failure.
402 — no usable licence#
{
"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:
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.
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. Not 401: the session is valid, the guess is not, and the web UI signs a user out on any 401. |
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": "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:
| 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 |
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:
| 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 — where 402 comes from.
- If it goes wrong