Endpoint index#
The agent registers 176 routes: 173 under /api/, plus GET /metrics — registered
either authenticated or not, depending on --metrics-public — and GET /health, and
GET / when --ui-dir is set.
The contract is generated from the agent's own route table, not from this page. Two
forms of it, both produced by the same code and both checked by CI against routes():
wheelhouse-agent endpoints # the same table, as Markdown
curl -s localhost:8080/api/openapi.json # OpenAPI 3.1, GET /api/openapi.jsonThis page is that list written out for reading, with request and response bodies given where they are not obvious. Where the two disagree, the generated one is right and this page has a bug — say so.
How to read the columns#
| Column | Meaning |
|---|---|
| Role | The minimum principal. — is unauthenticated. viewer < approver < operator < admin. |
| Licence | yes means the route answers 402 without a usable licence. fleet means it additionally needs a licence whose plan carries the fleet feature. |
| Redacts | The role below which secret leaves are blanked in the response. See What a read hides. |
Unauthenticated#
| Method | Path | Returns |
|---|---|---|
| GET | /health | {"status":"ok","version":"…"} — the agent's own version, and nothing else about the box. install.sh health-checks with it, and it is the only version answer available before a login. |
| GET | /api/auth/status | configured, tls, licensed, require_totp, and an oidc block with enabled, label, login and ready. No issuer URL. |
| GET | /api/oidc/status | enabled, label, login, ready, and on success redirect_uri; on failure a detail with the issuer's hostname masked out. |
| GET | /api/oidc/login | 302 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/callback | The 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.
| Method | Path | Role | Returns |
|---|---|---|---|
| POST | /api/auth/logout | viewer | Drops the session and clears the cookie. Returns logout_url for a single sign-on session whose provider supports RP-initiated logout. |
| GET | /api/auth/me | viewer | The current principal: user, role, kind, capabilities, csrf, expires_at, totp_enabled, must_change_password, email, display_name, oidc_subject, sso_only, via, and for an account its preferences: advanced, and pinned once the account has pinned or unpinned a page. |
| PATCH | /api/auth/preferences | viewer | Your own account's UI preferences, answered whole. {"advanced":true} turns Advanced mode on; {"pinned":["/firewall","/sessions"]} sets the pages pinned to the sidebar, in order: at most 12, each a path that starts with /, at most 64 characters, printable, none twice. [] pins nothing and is kept as an answer of its own; until the account first pins or unpins a page the field is left out and the UI shows its defaults. A field left out keeps its value; an unknown field, or a pinned entry that breaks a rule, is 400 naming the entry. An API token or the break-glass credential has no account to keep preferences on: 409. |
| POST | /api/auth/password | viewer | {"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/begin | viewer | A 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/confirm | viewer | {"code"} promotes the pending secret. |
| POST | /api/auth/totp/disable | viewer | Refused with 403 when the require_totp policy is on. |
| GET | /api/auth/sessions | viewer | Sessions. An admin sees every account's; everyone else sees only their own. Session ids are truncated to 12 characters. |
| DELETE | /api/auth/sessions/{id} | viewer | Revokes by id prefix — your own sessions, or anyone's if you are an admin. 404 when nothing matched. |
| POST | /api/auth/totp/recovery | viewer | Reissues your own ten single-use recovery codes and invalidates whatever was left of the old set. Costs a second factor — an authenticator code or one of the remaining recovery codes — because a printed list is a credential and an unlocked browser should not be able to print ten of them. The codes are in this response and nowhere else. |
| POST | /api/auth/oidc/unlink | viewer | Detaches 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: a read never answers 402.
| Method | Path | Redacts | Returns |
|---|---|---|---|
| GET | /api/version | — | The router's show version, as the router's own data. 502 if it cannot be reached. |
| GET | /api/system | — | One round trip for the router's identity and load, read by the Overview, the System page and the top bar: 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 admin | The 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 admin | The same subtree rendered as set lines, filtered from op-mode show configuration commands. |
| GET | /api/config/raw?path= | admin only | The configuration in the router's native curly-brace format. The backup export, unredacted; that is why it is admin-only rather than a redacted read. |
| GET | /api/interfaces | — | interfaces configuration plus the show interfaces summary text. |
| GET | /api/interfaces/stats | — | show interfaces, as the router's own data. |
| GET | /api/interfaces/detail | — | Per-interface link state, MTU, addresses, qdisc and RX/TX counters. One router read per configured interface, under the 20 s budget. |
| GET | /api/interfaces/offload | — | The nine interfaces ethernet <if> offload flags: the catalogue, and what the configuration asks for on every ethernet interface. With ?interface=<if> it also reads that one interface's kernel state from show interfaces ethernet <if> physical offload — one op-mode read for the interface named, never one per interface. A flag with no live value is not off; its note says why not. |
| 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/neighbors | — | The ARP/NDP table, parsed, plus the raw text. |
| GET | /api/routes | — | Both forwarding tables, IPv4 and IPv6, and the protocols static configuration. A configuration read that fails is static_note, never a null that reads as no static routes. |
| GET | /api/wan | — | The 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/vrrp | — | high-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/vrf | — | The vrf tree with each VRF's configured members, kernel state and IPv4 routing table. Four router calls whatever the VRF count; a router with no vrf node makes only the first. A read the router refused is reported as a note, never as an empty table. |
| GET | /api/lldp | — | service lldp configuration and what the neighbours announce. |
| GET | /api/vpn/ipsec | below admin | vpn ipsec configuration, strongSwan's security associations, and its connections. running:false when IPsec is not initialised. |
| GET | /api/vpn/openvpn | below admin | Every interfaces openvpn instance with the status text the router prints for each mode. |
| GET | /api/wireguard | every role | Tunnels and peers from the configuration, the state the router reports for each, each tunnel's public key, the address its devices were last told to connect to, and whether the input firewall drops its port. A tunnel's private key and a peer's pre-shared key read as [redacted] for every role, administrators included. |
| GET | /api/vpn/openvpn/clients | — | Each OpenVPN server's client certificates — the ones its authority signed for clients — with their expiry and whether they are revoked, the address clients were last told to connect to, and which servers' ports the input firewall drops. Certificates are public; no key is read out. |
| 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/leases | — | Active leases, parsed. |
| GET | /api/dhcp/reservations | — | The service dhcp-server subtree — pools, options and static mappings. |
| GET | /api/dns | every role | service dns configuration and the forwarder's cache statistics. A dynamic DNS password comes back as [redacted] whatever the role: the page only says whether one is set. |
| GET | /api/firewall/rules | — | The firewall subtree. |
| GET | /api/firewall/stats | — | show firewall statistics split into titled rulesets with parsed rows. |
| GET | /api/firewall/groups | — | Address, network, port and interface groups. |
| GET | /api/firewall/geoip | — | The country database behind firewall country matches: whether it is on the router, when it was written, its provider, and that the router refreshes it weekly. Read on the router only. |
| GET | /api/nat/rules | — | The nat subtree. |
| GET | /api/nat/stats | — | Per-rule destination and source NAT counters, parsed. |
| GET | /api/nat/one-to-one | — | The 1:1 NAT mappings — each a destination rule and the source rule that mirrors it on the same interface — and any nat static rule made outside Wheelhouse, which is listed and never written. |
| GET | /api/qos | — | The qos subtree. |
| GET | /api/qos/stats | — | Per shaped interface: the policy the configuration asks for beside the qdisc the kernel actually installed. |
| GET | /api/sessions | — | Connection-tracking flows with a top-talker aggregation. |
| GET | /api/conntrack/stats | — | Per-CPU connection-tracking counters since boot, and whether the table was under pressure in the last ten minutes — or only losing insert races, which a larger table does not prevent. |
| GET | /api/routing | — | BGP and OSPF: each one's configuration with every credential [redacted] for every role, and its sessions as FRR reports them. unknown with a note when the configuration could not be read. |
| GET | /api/ids | — | Suricata configuration, engine state, alerts, the rule set on the router (enabled rules and when they were fetched), the scheduled rule update, and inline-IPS status. |
| GET | /api/drift | below admin | Desired-state enforcement: managed, file, mode, drifted, ops, checked_at, applied_at, error, and the file's own header — schema and base, the router commit it was written against — so a diff says what it was measured against. mode:"off" when no file is being managed. |
| GET | /api/attention | — | What needs the operator, as sentences: level (clear, notice, warning or critical), headline ("All clear", or the most serious item, short), summary, items — each with kind, level, text and the page that explains it in link, and for a host probing the WAN side its address, port, count and dropped — then unavailable, what this router cannot judge, as sentences ("Firewall drops are not logged."), and checked_at. The sources are uplink health and failover (a health check whose targets have no route through their own uplink among them), interfaces with no link, the connection-tracking verdict, the licence, the IDS, WAN traffic against its band, logged firewall drops and the pending changes, each read the way its own page reads it. There is no "All clear" over a check that failed: whatever could not be read is an item of kind unchecked at level notice. A probe whose attempts were all dropped is clear. Computed when asked, at most every 15 seconds, and served to every caller in between. |
| GET | /api/daemons | — | The resolver, the DHCP server and UPnP: configured, not-configured or unsupported (this platform has no such node), told apart by asking the router's schema rather than by matching the words in its errors. enabled reports the disable node a configured-but-switched-off service carries, and is absent when nothing is configured. live is running, stopped or unknown from one read of show system processes for the whole page — unknown is not a synonym for stopped — and actions carries the exact set/delete each control stages. |
| GET | /api/apps | — | The router's containers: the declared set, the running set, the count and the raw table. |
| GET | /api/apps/catalog | — | The whole catalogue, plus source: where it came from. embedded is the copy compiled into the binary; signed is a verified catalogue index fetched from the download host. A fetched index that did not verify is not served, and its refusal says why. No catalogue signing key exists yet, so this says embedded on every router today. |
| GET | /api/apps/installed | — | Declared, running and health-probed, merged. widgets carries one number per installed app whose catalogue entry declares a stats URL and a JSON path to a single number; an app that declares none has no widget. |
| 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/containers | — | The same containers as /api/apps, shaped for the container view. |
| GET | /api/services | — | Every node under service, sorted, with its configuration. |
| GET | /api/processes | — | The router's process table, parsed. |
| GET | /api/pki | below admin | Certificate authorities and certificates with their expiry, from show pki and the pki subtree. The subtree carries private key material, so it is redacted. |
| GET | /api/ntp | — | Time synchronisation configuration and peer state. |
| GET | /api/ddns | every role | Dynamic DNS configuration and status, with each password as [redacted] whatever the role. |
| 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/interfaces/history/archive | — | The archived throughput series kept on the router: 24 hours at one-minute and 31 days at one-hour resolution, up to 32 interfaces, folded from the live sampler. |
| GET | /api/ipv6 | — | The IPv6 read side in one call: addresses with scope, the NDP table, the v6 forwarding table and the ip6 firewall counters. A read that fails degrades into notes rather than blanking the page. |
| GET | /api/ipv6/config | — | The IPv6 configuration: each interface's static, DHCPv6, SLAAC and EUI-64 addressing and prefix delegations, and the router advertisements per interface. |
| GET | /api/wan/probes | — | Round-trip time, jitter and loss per uplink, measured by the agent's own ICMP socket every five seconds, because the router exposes no per-target health to read. Each uplink also carries action: what the router would do about the numbers, which for an uplink with no check configured is nothing. |
| GET | /api/updates | — | Whether a newer release is published, what changed in it, and how to install it. Read from the channel index beside the releases and checked against the checksums published with it. The agent installs nothing, and says plainly when a release is unsigned. |
| GET | /api/openapi.json | — | The OpenAPI 3.1 description of every route, generated from the same table this page describes. |
| GET | /api/history | — | Commit revisions, parsed, plus the raw text. |
| GET | /api/history/diff?rev= | below admin | What one stored revision changed. rev must be a non-negative integer. |
| GET | /api/staged | below operator | The working set: count, the rendered commands, and the ops themselves. Redacted only below operator: an operator sees the values they staged. |
| GET | /api/stream | — | The telemetry WebSocket. See The telemetry stream. |
| GET | /api/license | — | Licence state, plan, features, expiry, grace, last refresh, server status and this router's fingerprint. |
| GET | /api/fleet | — | Every 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#
| Method | Path | Role | Licence | Returns |
|---|---|---|---|---|
| GET | /api/reconcile/file?file= | operator | no | Diffs 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= | viewer | fleet | One fleet router's configuration. Redacted below admin. 404 for an unknown id. |
| GET | /api/fleet/{id}/version | viewer | fleet | One fleet router's show version. |
| POST | /api/updates/check | operator | no | Checks the release channel now instead of waiting for the daily check. Operator rather than viewer because it is the one read that makes the router open a connection to the internet, and who may cause that is a policy question. It changes nothing. |
| GET | /api/audit/verify | admin | no | Re-reads the audit log from disk and reports whether its hash chain still adds up, naming the first entry that does not fit. The same answer says whether a copy is reaching the syslog collector. |
| GET | /api/audit/export | admin | no | The whole audit log, both generations, as JSON lines, for keeping the record somewhere this router cannot reach. Taking the log off the box is itself audited — after the body, so the copy does not contain the record of its own export. |
| GET | /api/support/bundle | admin | no | The redacted diagnostic archive: versions, configuration, router state, recent audit entries and the agent's log. Admin because it is more in one download than a viewer or an operator may read. |
| GET | /api/admin/oidc | admin | no | Provider state in full — including the issuer URL — the group mapping, and every linked account. |
| GET | /metrics | viewer, or none with --metrics-public | no | Prometheus 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.
| Method | Path | Licence | What it does |
|---|---|---|---|
| POST | /api/stage | yes | Appends 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. An operation the queue already holds is not queued again, and the queue is bounded twice — 10,000 operations and 16 MiB of them — with a request that would cross either bound refused whole. Returns the whole working set, not just what you added. |
| POST | /api/stage/remove | yes | {"index":N} removes one operation. 400 when there is nothing at that index. |
| POST | /api/commit | yes | Approver, not operator: it commits whatever is staged and carries no operations of its own. {"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/confirm | yes | Approver. Confirms a pending commit-confirm, cancelling the restart that would otherwise bring the router back on its saved configuration. |
| POST | /api/discard | yes | Empties the working set. |
| POST | /api/configure | yes | Applies an operation array directly, bypassing staging. Same body shape as /api/stage. |
| POST | /api/rollback | yes | Approver, for the same reason as /api/commit: reversing a commit is the other half of confirming one. {"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/save | yes | {"file":"…"} writes the running configuration to a file on the router; empty means the boot configuration. |
| POST | /api/config/load | yes | {"file":"…"} loads a configuration file that already exists on the router, and commits it. |
| POST | /api/reconcile | yes | {"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/capture | no | Starts a bounded packet capture on one interface: tcpdump on the router, written to a single pcap under the agent's data directory, bounded by packets, seconds and bytes at once. Answers 501 only off-router or with no tcpdump. No licence — a capture changes no configuration. |
| GET | /api/capture | no | Viewer. The running or last capture: state, progress and when its file is deleted. |
| POST | /api/capture/stop | no | Stops the running capture, keeping what it caught. 409 if it had already ended. |
| DELETE | /api/capture | no | Deletes the pcap now rather than waiting out its retention. |
| GET | /api/capture/download | no | The pcap, cut on a packet boundary. Operator, because the body is raw packet data. 409 while it is still running. |
| POST | /api/diagnostics/traceroute | no | {"host":"…"} → the router's own traceroute output. A command, not a configuration change, so it needs the operator role and no licence. |
| POST | /api/diagnostics/wake-on-lan | no | {"interface":"eth0","mac":"00:53:00:12:34:56"} → {"sent":true,…} once the router has sent a Wake-on-LAN magic packet out of that interface. A command, not a configuration change, so it needs the operator role and no licence; every attempt is audited as wake-on-lan, refusals included. 400 for a MAC address that is not six colon-separated hex pairs, or an interface that is not one of the router's. 501 when the agent is not on the router, or the router cannot run the command. 502 when the router's interfaces could not be read or the router printed a failure. |
| POST | /api/ids/ips/plan | no | Renders the operations that would put the inline IPS engine in the forward path, with warnings, without applying them. |
| POST | /api/vrf/plan | no | Renders the operations that create, change or delete one VRF and bind its interfaces, without applying them: a free table, interfaces at their own node, members released on delete, and a refusal while something still sends traffic to the VRF. |
| POST | /api/ids/rules/update | yes | Runs the router's own update suricata: fetches the rules, tests them and restarts the engine. On the router only, bounded to ten minutes, audited whether it runs or is refused; 409 while one is running or when intrusion detection is off, 501 off the router. Returns the updater's counts — total, enabled, added, removed, modified. |
| POST | /api/firewall/geoip/update | yes | Runs the router's own update geoip: downloads the country database and refreshes every country match in use. On the router only, bounded to ten minutes, audited; 409 while one is running, 501 off the router. |
| POST | /api/nat/one-to-one/plan | no | Renders the operations that create, change or remove one 1:1 NAT mapping as a destination and source rule pair, numbered ahead of the rules on its interface that would take its traffic, with warnings. Refuses the router's own address and addresses already mapped. Stages nothing. |
| POST | /api/vpn/wireguard/tunnel | yes | Adds a WireGuard tunnel to the working set with a key pair the agent generates. The private key is staged sealed: no answer, log or audit entry carries it, and the working set shows [redacted] for every role. With open_firewall, the input firewall rule for the port is added too when the port would be dropped. Answers with the public key. |
| POST | /api/vpn/wireguard/client | no | Plans a remote device on a tunnel: its key pair, an optional pre-shared key, the first free address in each of the tunnel's networks, and its configuration file with the router as its peer. The device's private key is in the answer and nowhere else — not on the router, in the agent's state, a log or the audit trail. Stages nothing; the page stages the peer. |
| POST | /api/vpn/input-rule | no | Plans the input firewall rule that accepts a VPN's listen port — after the chain's accept rules, before the first rule that does anything else — or says the port is already accepted. Stages nothing. |
| POST | /api/vpn/openvpn/clients | yes | Adds a client certificate for an OpenVPN server to the working set, signed by the server's authority, whose key must be on the router. The client's private key is staged sealed, like a WireGuard tunnel's. |
| POST | /api/vpn/openvpn/clients/revoke | no | Plans a client's revocation: the certificate marked revoked, and a new revocation list signed by the server's authority naming it beside every certificate that authority revoked before. Stages nothing. |
| POST | /api/vpn/openvpn/tls-key | yes | Adds a new tls-crypt or tls-auth key for an existing OpenVPN instance to the working set, staged sealed, with the instance pointed at it. |
| POST | /api/vpn/openvpn/plan | no | Renders the operations that would build one OpenVPN instance, without applying them. The certificate material comes from the router's own pki tree: named as it stands, minted here, or pasted in and checked — the certificate against the CA, the key against the certificate. Operator rather than viewer because the answer can carry a private key; no licence, because nothing is staged and nothing is applied. |
| POST | /api/dhcp/scope/plan | no | Renders the operations that add, change, move or delete one DHCP scope, without applying them. A new scope gets the next free subnet id, counting pending changes; an edit stages only the fields that changed; a new subnet or network name carries the reservations and every other setting the form does not manage to the new node. 400 names the field it refuses, or the reservations a new subnet would leave outside it. |
| POST | /api/apps/plan | yes | Renders the operations an app install would stage, plus warnings, the image and whether it is already on the router. |
| POST | /api/apps/pull | yes | Pulls 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/prepare | yes | Creates the host directories and files a container's bind mounts need. On-router it creates them; off-router it returns the exact command to run there. |
| POST | /api/apps/update | yes | Re-pulls an installed app's image and reports whether the image id changed. |
| POST | /api/daemons/{name}/restart | yes | Bounces one companion — dns or dhcp — through the router's own op-mode restart, which touches no configuration, so there is nothing to put in a diff. A platform whose API has no such operation answers 501 with the router's own words and the two commits that do the same job by hand. 409 for a service that is unconfigured or switched off; enable it instead. |
| POST | /api/containers/{name}/restart | yes | Restarts one container through the router's own restart container command. Not a configuration change: nothing is staged, nothing is committed and no revision is written — which is what separates it from /api/apps/restart. The platform has this one operational verb for containers and no start or stop, so a stopped container is refused with 409 and the single command that starts it. An agent that is not on the router answers 501 with the two ordered commits that do the same job by hand. name must match the container-name pattern. |
| POST | /api/apps/restart | yes | {"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/feature | yes | Enables or disables a built-in feature module, such as the WireGuard page. |
| DELETE | /api/apps/image | yes | Removes an unused image from the router. |
| POST | /api/fleet/{id}/configure | fleet | Applies an operation array to one fleet router, bypassing that router's queue. |
| POST | /api/fleet/{id}/stage | fleet | Stages operations onto one router's own queue. An operation the queue already holds is not queued again; added says how many were. 400, refusing the request whole, past either bound on the queue: 10,000 operations, or 16 MiB of them. |
| GET | /api/fleet/{id}/staged | fleet | Viewer. That router's queue, ops beside commands. Secrets are redacted below operator in both. |
| POST | /api/fleet/{id}/stage/remove | fleet | {"index":N} un-stages one operation from that router's queue. |
| POST | /api/fleet/{id}/discard | fleet | Empties that router's queue, audited as fleet-discard with what it threw away. |
| POST | /api/fleet/{id}/commit | fleet | Approver. Commits that router's queue against that router. Answers committed, the number of operations sent. confirm_minutes > 0 arms that router's own commit-confirm; awaiting_confirm says so. |
| POST | /api/fleet/{id}/commit/confirm | fleet | Approver. Confirms a commit-confirm armed on that router — the router the window belongs to, and no other; the local /api/commit/confirm would let it reboot and cancel a window here. Audited as fleet-commit-confirm. 502 if the router had no window pending. |
Admin plane#
All admin. None needs a licence: accounts and tokens can be managed on an
unlicensed router.
| Method | Path | What it does |
|---|---|---|
| POST | /api/vpn/openvpn/client-config | One client's OpenVPN configuration file (.ovpn), assembled from the server's configuration and the certificate store: the transport, the address and port to connect to, the ciphers and hash, the authority's certificate, the client's certificate and private key, and the server's TLS key. No other private key the router holds is ever in it. Audited, including a refusal. |
| GET | /api/admin/users | Every 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/tokens | Token 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/settings | session_ttl_minutes, require_totp, audit_retention, commit_confirm_minutes. The stored licence key is blanked out of this response. |
| PUT | /api/admin/settings | The 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. |
| GET | /api/system/factory-reset | Whether this router can be factory-reset from here (available, reason), its host name, and its configuration choices: keep, and default or installed where the router has them. |
| POST | /api/system/factory-reset | {"confirm":"<host name>","current_password":"…","config":"keep"\|"default"\|"installed","apps":false,"new_password":"…"}. Needs an account with a password, not a token. Starts the rescue tool's factory reset in a unit of its own and answers 202; new_password is carried as a hash and becomes admin's password. Audited before it starts. Factory reset |
| 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. |
| POST | /api/admin/users/{name}/totp/clear | Takes the second factor off another account — the lost phone with no recovery codes left. Never your own: an admin who still holds their authenticator disables it from Account, and one who does not is what another admin is for. Refused when there is nothing to clear. The target's sessions go with the factor, and require_totp is untouched, so the account comes back enrolment-gated. |
| PUT | /api/admin/updates | Switches the release-channel check on or off and chooses the channel. Off means no request is ever made, which is what a box that must not talk to the internet needs. The same switch is the enabled field of <data-dir>/updates.json, so it can be set before anyone has signed in. |
| GET | /api/admin/gitexport | The git-backed configuration export: its settings, its last outcome, and the router's public key. The private half never leaves the router and is on no response; public_key is the line to install on the remote as a deploy key with write access. |
| PUT | /api/admin/gitexport | Configures the export. Refused without a parseable SSH remote and a pinned host key: an export that trusts whatever answers the port hands the configuration to whoever answers it. There is no trust-on-first-use and no flag that adds one. |
| POST | /api/admin/gitexport/key | Generates this router's ed25519 export key, owner-only in the data directory. It refuses to overwrite an existing key with 409, because a key that regenerates is a router that silently loses its access to the remote. |
| POST | /api/admin/gitexport/push | Exports the running configuration now. Secrets are redacted before the file is committed, so the repository is a history and not a restorable backup. A push that finds the redacted configuration unchanged answers skipped rather than committing an empty revision. 409 when no export is configured; 502 when the remote refused or could not be reached. |
| 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/license | Removes the key. The write plane closes at once. |
The UI#
| Method | Path | Notes |
|---|---|---|
| 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#
- Role matrix — the same list, sorted by who may call it.
- Status codes — what each failure means.
- Authenticating — how to become a principal.
- What a read hides — the Redacts column, explained.
- The telemetry stream
- Audit entries — what a write leaves behind.