Wheelhouse docs

Endpoint index#

The agent registers 108 routes in one function, routes() in agent/main.go, and the minimum role is written on each registration rather than inside the handler. Two of the 108 are the same path — GET /metrics is registered either authenticated or not, depending on --metrics-public — so 107 distinct method-and-path pairs answer on a running agent, plus GET / when --ui-dir is set. This page is the whole list.

Re-derive it for the version you are running:

bash
grep -o 'mux.HandleFunc("[A-Z]* [^"]*"' agent/main.go

There is no OpenAPI document and nothing generates one, so this table is the contract. Request and response bodies are given where they are not obvious; for the rest, read the handler or drive the web UI with the browser's network tab open.

How to read the columns#

ColumnMeaning
RoleThe minimum principal. is unauthenticated. viewer < operator < admin.
Licenceyes means the route answers 402 without a usable licence. fleet means it additionally needs a licence whose plan carries the fleet feature.
RedactsThe role below which secret leaves are blanked in the response. See What a read hides.

Unauthenticated#

MethodPathReturns
GET/health{"status":"ok"} and nothing else. Not the version — anonymous version disclosure was closed deliberately. install.sh health-checks with it.
GET/api/auth/statusconfigured, tls, licensed, require_totp, and an oidc block with enabled, label, login and ready. No issuer URL.
GET/api/oidc/statusenabled, label, login, ready, and on success redirect_uri; on failure a detail with the issuer's hostname masked out.
GET/api/oidc/login302 to the identity provider. ?next=/path is remembered for after the login (paths only). 503 when single sign-on is not configured.
GET/api/oidc/callbackThe provider's return leg. No session exists until the ID token verifies.
POST/api/auth/login{"username","password","totp"} → a session cookie and a CSRF token. Behind all three rate limiters.

Session self-service#

Every route here is viewer, because it acts on the caller's own account.

MethodPathRoleReturns
POST/api/auth/logoutviewerDrops the session and clears the cookie. Returns logout_url for a single sign-on session whose provider supports RP-initiated logout.
GET/api/auth/meviewerThe current principal: user, role, kind, capabilities, csrf, expires_at, totp_enabled, must_change_password, email, display_name, oidc_subject, sso_only, via.
POST/api/auth/passwordviewer{"current_password","new_password"}. A wrong current password is 403, not 401, so the UI does not read it as a dead session. Every other session for that account is dropped.
POST/api/auth/totp/beginviewerA fresh secret and its otpauth:// URI. Stored as pending: an enabled second factor keeps gating login until a code proves the new one.
POST/api/auth/totp/confirmviewer{"code"} promotes the pending secret.
POST/api/auth/totp/disableviewerRefused with 403 when the require_totp policy is on.
GET/api/auth/sessionsviewerSessions. An admin sees every account's; everyone else sees only their own. Session ids are truncated to 12 characters.
DELETE/api/auth/sessions/{id}viewerRevokes by id prefix — your own sessions, or anyone's if you are an admin. 404 when nothing matched.
POST/api/auth/oidc/unlinkviewerDetaches your own account from the identity provider. Refused if it would leave you with no way in.

Read plane#

All viewer. None needs a licence — losing sight of a router because a card expired would be a worse product than losing the ability to change it.

MethodPathRedactsReturns
GET/api/versionThe router's show version, as the router's own data. 502 if it cannot be reached.
GET/api/systemThe Dashboard's single round trip: hostname, parsed version, uptime with three load averages, memory, storage, cpus, boot images, and the raw text of each. Seven reads, capped at the 20 s router budget and cancelled if the browser goes away.
GET/api/config?path=below adminThe configuration tree as JSON. path may be a slash-joined string or a JSON array — the array form is how a node whose key holds a slash (subnet 10.0.0.0/16) is named.
GET/api/config/commands?path=below adminThe same subtree rendered as set lines, filtered from op-mode show configuration commands.
GET/api/config/raw?path=admin onlyThe configuration in the router's native curly-brace format. This is the backup export, unredacted, which is why it is an admin route rather than a redacted read.
GET/api/interfacesinterfaces configuration plus the show interfaces summary text.
GET/api/interfaces/statsshow interfaces, as the router's own data.
GET/api/interfaces/detailPer-interface link state, MTU, addresses, qdisc and RX/TX counters. One router read per configured interface, under the 20 s budget.
GET/api/interfaces/history?points=The server-sampled throughput series: interval_ms and, per interface, up to points samples (default 120, 240 kept).
GET/api/neighborsThe ARP/NDP table, parsed, plus the raw text.
GET/api/routesThe kernel forwarding table and the declared static routes (IPv4).
GET/api/wanThe multi-WAN model: every uplink ranked and joined with the live default routes, failover verdicts, load-balancer health, policy tables, per-uplink source NAT, and suggested free rule and table numbers.
GET/api/vrrphigh-availability configuration, keepalived's per-group state from show vrrp, and service conntrack-sync. running:false when keepalived says data is not available.
GET/api/lldpservice lldp configuration and what the neighbours announce.
GET/api/vpn/ipsecbelow adminvpn ipsec configuration, strongSwan's security associations, and its connections. running:false when IPsec is not initialised, which is the honest state of a router without it.
GET/api/vpn/openvpnbelow adminEvery interfaces openvpn instance with the status text the router prints for each mode.
GET/api/wireguardbelow adminTunnels and peers from the configuration, plus the reported interfaces.
GET/api/diagnostics/lookup?host=show host lookup through the router's own resolver, with the addresses parsed out. The host must match [A-Za-z0-9.:_-]{1,253}.
GET/api/dhcp/leasesActive leases, parsed.
GET/api/dhcp/reservationsThe service dhcp-server subtree — pools, options and static mappings.
GET/api/dnsservice dns configuration and the forwarder's cache statistics.
GET/api/firewall/rulesThe firewall subtree.
GET/api/firewall/statsshow firewall statistics split into titled rulesets with parsed rows.
GET/api/firewall/groupsAddress, network, port and interface groups.
GET/api/nat/rulesThe nat subtree.
GET/api/nat/statsPer-rule destination and source NAT counters, parsed.
GET/api/qosThe qos subtree.
GET/api/qos/statsPer shaped interface: the policy the configuration asks for beside the qdisc the kernel actually installed.
GET/api/sessionsConnection-tracking flows with a top-talker aggregation.
GET/api/conntrack/statsPer-CPU connection-tracking health: insert failures and early drops.
GET/api/routingWhether BGP and OSPF are configured, and their configuration.
GET/api/idsSuricata configuration, engine state, alerts, and inline-IPS status.
GET/api/driftbelow adminDesired-state enforcement: managed, file, mode, drifted, ops, checked_at, applied_at, error. mode:"off" when no file is being managed.
GET/api/daemonsThe configuration state of service upnp, service dns and service dhcp-server, each configured or not-configured.
GET/api/appsThe router's containers: the declared set, the running set, the count and the raw table.
GET/api/apps/catalogThe whole embedded catalogue.
GET/api/apps/installedDeclared, running and health-probed, merged.
GET/api/apps/hints/{page}The integration offers installed apps make on one page.
GET/api/apps/logs/{name}One container's journal. name must match ^[-a-zA-Z0-9]+$ or the answer is 400.
GET/api/containersThe same containers as /api/apps, shaped for the container view.
GET/api/servicesEvery node under service, sorted, with its configuration.
GET/api/processesThe router's process table, parsed.
GET/api/pkiCertificate authorities and certificates with their expiry, from show pki and the pki subtree.
GET/api/ntpTime synchronisation configuration and peer state.
GET/api/ddnsDynamic DNS configuration and status.
GET/api/log?lines=The journal tail. Default 100 lines, capped at 5000 — this is a page, not a log export.
GET/api/audit?limit=The agent's own audit log, newest first. Default 300.
GET/api/historyCommit revisions, parsed, plus the raw text.
GET/api/history/diff?rev=below adminWhat one stored revision changed. rev must be a non-negative integer.
GET/api/stagedbelow operatorThe working set: count, the rendered commands, and the ops themselves. Redacted only below operator — an operator who staged a key typed it, and a Commit Bar that shows them [redacted] is not a preview.
GET/api/streamThe telemetry WebSocket. See The telemetry stream.
GET/api/licenseLicence state, plan, features, expiry, grace, last refresh, server status and this router's fingerprint.
GET/api/fleetEvery router in the fleet file, health-checked concurrently. Answers enabled:false with a note when fleet mode is off or the licence lacks the feature — this route itself never 402s.

Read plane, licensed or operator-only#

MethodPathRoleLicenceReturns
GET/api/reconcile/file?file=operatornoDiffs a desired-state document in the data directory against the router. file is a bare name (or an absolute path inside the data directory) ending .yaml, .yml or .json, and never state.*. Default agent.yaml.
GET/api/fleet/{id}/config?path=viewerfleetOne fleet router's configuration. Redacted below admin. 404 for an unknown id.
GET/api/fleet/{id}/versionviewerfleetOne fleet router's show version.
GET/api/admin/oidcadminnoProvider state in full — including the issuer URL — the group mapping, and every linked account.
GET/metricsviewer, or none with --metrics-publicnoPrometheus text exposition. See Prometheus metrics.

Write plane#

Operator role and a usable licence, except the two rows marked otherwise. Every one of these writes an audit entry with the actor, the role, the source address and the outcome.

MethodPathLicenceWhat it does
POST/api/stageyesAppends operations to the working set. The body is a bare array of {"op":"set"\|"delete","path":[…]}. Any other verb, an empty path, or an empty element in a path is 400 naming the index. Returns the whole working set, not just what you added.
POST/api/stage/removeyes{"index":N} removes one operation. 400 when there is nothing at that index.
POST/api/commityesCommits the working set. {"confirm_minutes":N} arms the router's native commit-confirm. Returns committed counted from the payload actually sent. 400 when nothing is staged.
POST/api/commit/confirmyesConfirms a pending commit-confirm, cancelling the reboot that would otherwise restore the previous configuration.
POST/api/discardyesEmpties the working set.
POST/api/configureyesApplies an operation array directly, bypassing staging. Same body shape as /api/stage.
POST/api/rollbackyes{"revision":N}, default 1. Loads /config/archive/config.boot.<N>.gz and commits it — which is exactly what the CLI's rollback does, because the router's HTTP API has no rollback operation. Revision 0 is the running configuration.
POST/api/config/saveyes{"file":"…"} writes the running configuration to a file on the router; empty means the boot configuration.
POST/api/config/loadyes{"file":"…"} loads a configuration file that already exists on the router, and commits it.
POST/api/reconcileyes{"desired_state":{…},"full":bool,"apply":bool}. Without apply it returns the operations and any warnings; with it, they are added to the working set. It never commits — the Commit Bar stays the single write point. 400 on an empty document.
POST/api/captureyesNot implemented. Answers 501 with the monitor traffic command to run by hand.
POST/api/diagnostics/tracerouteno{"host":"…"} → the router's own traceroute output. A command, not a configuration change, so it needs the operator role and no licence.
POST/api/ids/ips/plannoRenders the operations that would put the inline IPS engine in the forward path, with warnings, without applying them.
POST/api/apps/planyesRenders the operations an app install would stage, plus warnings, the image and whether it is already on the router.
POST/api/apps/pullyesPulls an image onto the router. The router refuses to commit a container whose image is not local, so this runs before the configuration change. Image references are validated against the OCI grammar.
POST/api/apps/prepareyesCreates the host directories and files a container's bind mounts need. On-router it creates them; off-router it hands back the exact command instead of pretending.
POST/api/apps/updateyesRe-pulls an installed app's image and reports whether the image id changed.
POST/api/apps/restartyes{"name":"…"}. Bounces a container as two commits — set container name <n> disable, then delete it. There is no control socket. 409 if it is already stopped. If the second commit fails the app is left down and the error says so in those words.
POST/api/apps/featureyesEnables or disables a built-in feature module, such as the WireGuard page.
DELETE/api/apps/imageyesRemoves an unused image from the router.
POST/api/fleet/{id}/configurefleetApplies an operation array to one fleet router.

Admin plane#

All admin. None needs a licence — an unlicensed router must still be able to manage its own accounts.

MethodPathWhat it does
GET/api/admin/usersEvery account: name, role, TOTP, must-change-password, created, last login, and whether it is linked to the identity provider or has a password at all.
POST/api/admin/users{"name","password","role"}. Names match ^[A-Za-z0-9._-]{1,64}$; admin-token and anything starting token: are reserved. 409 if the name exists. New accounts are flagged must-change-password.
PATCH/api/admin/users/{name}role, password, oidc_subject, email — each optional. Demoting or deleting the last admin is 409. Any of these changes drops that account's sessions. Linking a subject another account claims is 409.
DELETE/api/admin/users/{name}409 on your own account, and 409 on the last admin.
GET/api/admin/tokensToken metadata. Never the token.
POST/api/admin/tokens{"label","role","expires_days"}. The plaintext is returned exactly once; only its SHA-256 is stored.
DELETE/api/admin/tokens/{id}Revokes one token. 404 if there is no such id.
GET/api/admin/settingssession_ttl_minutes, require_totp, audit_retention, commit_confirm_minutes. The stored licence key is blanked out of this response.
PUT/api/admin/settingsThe same four. Session TTL must be 5 minutes to 30 days; the commit-confirm window 1 to 60 minutes; audit retention below 100 is raised to 100. The stored licence key is preserved across the write.
POST/api/system/power{"action":"reboot"\|"poweroff"}. Not a configuration change, so not staged. Audited.
POST/api/system/image{"op":"add","url":…}, or {"op":"delete"\|"set_default","name":…}, or {"op":"show"}. add accepts http(s) URLs only and downloads and installs before answering; names must match ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$. Audited except for show.
PUT/api/admin/license{"key":"WHL1.…"}. Verified before it is stored, then refreshed against the licence server immediately so the page can show the answer.
DELETE/api/admin/licenseRemoves the key. The write plane closes at once.

The UI#

MethodPathNotes
GET/Registered only when --ui-dir is set. Serves the directory, falls back to index.html for any path that is not a real file, so the single-page app's own routes work on a hard refresh.

See also#

Checked against#

agent/main.go (routes and the handlers in it), agent/opmode.go, agent/apps.go, agent/admin.go, agent/authhttp.go, agent/parity.go, agent/gaps.go, agent/ids.go, agent/wan.go, agent/fleet.go, agent/desired.go, agent/reconcile.go, agent/license.go, agent/endpoints_test.go, docs/deploy.md "Agent endpoints".

Updated 2026-09-02 api endpoints http