Wheelhouse docs

Your first API call#

You will end up with a working curl against the router's API, and the four facts you need before writing anything larger.

Before you start#

Step 1 — Prove the agent is alive#

bash
R=https://<router>:8443
curl -sk "$R/health"
json
{"status":"ok","version":"0.5.1"}

That route is unauthenticated, so a load balancer can use it, and it carries the agent's version. Nothing else about the box is in it: no host name, no licence state, no router reachability. GET /api/system carries those, behind a principal.

Step 2 — Authenticate#

bash
T=wh_...
curl -sk -H "Authorization: Bearer $T" "$R/api/auth/me"

That returns your principal, its role and its capabilities, which is the fastest way to confirm a token has the role you thought.

Step 3 — Read something#

bash
curl -sk -H "Authorization: Bearer $T" "$R/api/system"
curl -sk -H "Authorization: Bearer $T" "$R/api/interfaces/detail"
curl -sk -H "Authorization: Bearer $T" "$R/api/config/commands"
curl -sk -H "Authorization: Bearer $T" "$R/api/audit?limit=20"

Four properties of this API#

1. Use a token, not a cookie. Every mutating request from a browser session must also echo that session's CSRF token in X-Wheelhouse-CSRF; a cookie alone can never change configuration. Token requests skip that requirement, which is why scripts use tokens.

2. Reads never need a licence; writes answer 402 without one. Every mutating endpoint answers 402 with the reason on an unlicensed router, and every read keeps working. Build for that: a monitoring integration survives a lapsed subscription.

3. Configuration reads are redacted below the admin role. Private keys, pre-shared secrets and password hashes come back as [redacted]. Redaction matches secret leaf names, so a value that is a secret because of where it sits — a container's environment variable, an SNMP community string — is not matched. Assume a viewer can read those.

4. The router describes its own API. GET /api/openapi.json serves OpenAPI 3.1 built from the agent's route table — every path, method, parameter, status code and minimum role, taken from the code that enforces them rather than from a document somebody maintains:

bash
curl -sk -H "Authorization: Bearer $T" "$R/api/openapi.json" | python3 -m json.tool | head

It is a description of the surface, not of the payloads: request bodies are one line of prose against a bare object schema, and responses are not schema'd. So it will tell you a route exists, what it takes in the query string and who may call it, and it will not tell you the shape of the JSON. For that, drive the UI with the browser's network tab open. The endpoint reference is generated from the same table.

The status codes that mean something specific here#

CodeMeans
401No principal — a missing or wrong token, or a browser session not echoing X-Wheelhouse-CSRF
402No usable licence, on a write
403Your role is not enough for this route. Recorded in the audit log as denied <method> <path>.
409A conflict — for example, restarting an app that is already stopped
501POST /api/capture, only when the agent is not on the router or the router has no tcpdump; the body hands back the monitor traffic command to run there.
502The agent could not reach the router. Check --api-url and the API key.

Step 4 — Watch the live stream, if you want it#

bash
# a WebSocket, so a plain curl will not do it
wscat -c "wss://<router>:8443/api/stream" -H "Authorization: Bearer $T"

GET /api/stream is read uncached each tick. An unreadable router arrives as {"type":"error","error":…} rather than as silence, which is the difference between "the router is quiet" and "the router is gone".

WebSocket upgrades are rejected unless the Origin matches.

Check it worked#

Every command above returning JSON rather than an error. Then:

bash
curl -sk -H "Authorization: Bearer $T" "$R/api/audit?limit=5"

Your reads are not in the audit log — it records mutations — but a denied request is, which makes it the place to look when a call is refused and you cannot see why.

See also#

Updated 2026-09-03 api automation curl