Wheelhouse docs

API tokens#

Admin → API tokens issues the credentials that scripts, CI jobs and configuration tools use. The page's subtitle: "Tokens for scripts and automation to use the API, each with its own role." A token carries one of the four roles, an expiry if you give it one, and the label you gave it. Only a hash of the token is stored, and the token itself is shown exactly once.

The page is admin-only. Nothing here goes to the pending changes: issuing and revoking take effect at once, and both are written to the audit log.

The web interface uses the same API, so a token can do what a signed-in person with the same role can do, without a browser.

The table#

The Tokens panel has one row per token. The token itself is never in it.

ColumnWhat it shows
LabelWhat you named it.
IDTwelve hexadecimal characters. This identifies the token; it is not the token.
RoleViewer, Approver, Operator or Admin.
Issued byWho issued it.
IssuedHow long ago.
Last usedHow long ago, or Never. It is updated at most once a minute per token.
ExpiresA date, or Never. Shown in amber within seven days of the date.
Revoke — see Revoking.

With no tokens, the panel says "No API tokens" and "Issue a token for a script or an automation system to use the API."

Issuing one#

Issue token opens the Issue API token panel.

FieldWhat it takes
LabelThe hint: "Name the system that will use this token." — terraform-prod, backup-cron — rather than the person. Required: Issue token stays disabled without one, and the agent refuses a token without a label too.
RoleViewer: reads everything, Approver: applies changes others prepare, Operator: prepares and applies changes, or Admin: also manages users, tokens and settings.
ExpiresNever, In 30 days, In 90 days or In a year.

Choosing Admin adds a line in amber: "An admin token can add users and issue more tokens." Most automation needs no more than Operator, which covers changing the router and nothing that changes who may.

When the token is issued, a panel titled New token: followed by the label appears above the table:

Copy the token now. It is not shown again.

The token is below it, with a copy button. Done closes the panel, and the token is gone from the page: a lost token is revoked and a new one issued, not recovered. In Advanced mode the panel also shows an Example request built for this router:

bash
curl -H "Authorization: Bearer wh_…" https://<router>:8443/api/config | jq .

What a token looks like, and what is kept#

  • A token is wh_ followed by 48 hexadecimal characters — 24 random bytes. The wh_ prefix makes a leaked token easy to find in a log and to catch with a secret scanner.
  • What is stored is the SHA-256 of the token, its ID, its label, its role, who issued it and when, when it was last used and when it expires.
  • A token is looked up by comparing hashes in constant time, and an expired token is treated as no token at all.

The file that holds all of this is readable by root alone, in the agent's data directory.

Using one#

bash
R=https://<router>:8443
T=wh_…

curl -sk -H "Authorization: Bearer $T" $R/api/firewall/rules | jq .

A token is sent in the Authorization header. Two things follow from that:

  • A token needs no CSRF header. The CSRF requirement belongs to the browser's cookie, which a browser sends automatically; a script sends a header on purpose. So a token can add to the pending changes with a single request, and a cookie cannot.
  • A token in the URL is accepted on one route only. /api/stream takes ?token=, because a browser WebSocket cannot set headers. Everywhere else the agent refuses it and says why: a credential in a URL ends up in proxy logs, browser history and Referer headers.

Adding to the pending changes and applying them with a token takes the same requests the web interface makes — here with the auto-revert window of two minutes, then keeping the change:

bash
curl -sk -X POST $R/api/stage -H "Authorization: Bearer $T" \
  -H 'Content-Type: application/json' \
  -d '[{"op":"set","path":["firewall","ipv4","forward","filter","rule","110","action","accept"]}]'
curl -sk -X POST $R/api/commit -H "Authorization: Bearer $T" \
  -H 'Content-Type: application/json' -d '{"confirm_minutes":2}'
curl -sk -X POST $R/api/commit/confirm -H "Authorization: Bearer $T"

The first request needs an operator token. The other two need at least an approver token.

What a token may do#

Exactly what its role may do — see Roles. The same rules, and the same 402 when the router has no usable licence and the request would change the router.

Two differences from a browser session:

  • A token is never held up by the two-factor requirement. Require two-factor authentication applies to browser sessions. A token, or the break-glass token, is the way back when somebody has locked themselves out of the web interface.
  • A token is its own principal. It appears in the audit log as token:<label>, not as the person who issued it — which is why the label matters, and why account names beginning token: are refused.

Revoking#

Revoke asks "Revoke <label>?" before it acts — "Anything using this token stops working at once" when you hover over it. Whatever was using the token is refused on its next request, and there is no way to bring the token back. The message afterwards says the label can no longer be used, and the audit log records revoke-token with the token's ID.

Tokens are independent of accounts. Deleting the account that issued a token does not revoke it; the token keeps its own role until it is revoked or expires. Check both lists when somebody leaves.

The break-glass token is not one of these#

The agent can be given a break-glass token: a file holding a token that authenticates as an admin with no account behind it. It is not issued here, does not appear in this table, and cannot be revoked from the page. To remove it, delete the file at the router's console, then restart the agent — Restart agent on Agent settings does the restart. The installed service looks for /config/wheelhouse/admin-token, and nothing creates that file, so a new router has no break-glass token at all.

For automation#

GET /api/admin/tokens lists the tokens, without the tokens themselves. POST /api/admin/tokens issues one — label, role, and expires_days (0 for never) — and its answer is the only place the token ever appears. DELETE /api/admin/tokens/{id} revokes one, and answers 404 for an ID that is not there. All three are admin-only.

See also#

Updated 2026-09-18 manual tokens api automation