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#
- An API token with the role the call needs —
Issue a token for automation. Start with
viewer. - The router's address.
Step 1 — Prove the agent is alive#
R=https://<router>:8443
curl -sk "$R/health"{"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#
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#
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:
curl -sk -H "Authorization: Bearer $T" "$R/api/openapi.json" | python3 -m json.tool | headIt 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#
| Code | Means |
|---|---|
401 | No principal — a missing or wrong token, or a browser session not echoing X-Wheelhouse-CSRF |
402 | No usable licence, on a write |
403 | Your role is not enough for this route. Recorded in the audit log as denied <method> <path>. |
409 | A conflict — for example, restarting an app that is already stopped |
501 | POST /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. |
502 | The agent could not reach the router. Check --api-url and the API key. |
Step 4 — Watch the live stream, if you want it#
# 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:
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.