The management API
Drive governance over HTTP with the endpoints grouped by job, the role each one needs, and the behaviours no schema records.
On this page
You want to mint a key from a provisioning script, raise one customer’s cap from your own admin tool, or graph how close each team is to its monthly dollars. All of that is HTTP on the admin plane, under one path prefix, behind one credential.
This page teaches the shape, not the fields. The rendered API reference already carries every parameter, schema and status code, so what follows is how the operations group, what role each group needs, and the handful of behaviours a caller cannot read off a schema. If limits, tenancy nodes and virtual keys are new, start at governance.
Where the API lives, and how you authenticate
The governance API rides the dashboard, and the dashboard is off unless you ask for it, so start with the port, and with whether anything is answering on it.
Everything below is served on the admin listener, server.admin, default 127.0.0.1:9090,
alongside the dashboard UI, and every path in this section is relative to
/tt/api/v1/governance. The data plane, server.listen, default 127.0.0.1:8787, carries
none of it.
Both surfaces are gated by dashboard.enabled, which is false in a config file until you
set it. The zero-config paths turn it on for you: tokentriage demo, and the --anthropic
and --simple/--complex quickstarts of tokentriage run unless you pass --no-dashboard.
dashboard.auth.mode then decides the credential. Its default is off, which is valid only
on a loopback admin bind and treats every caller as a local admin. Set it to token and a
bearer is required:
$ tokentriage token new --role admin --label docs-capture
token id: 39d61563c5ab79d8 role: admin
This token is shown ONCE and cannot be recovered:
ttk_tQIzM0aMxJtSY9gcFn-T3k5Ne3Br…
That capture is from a source build against a throwaway state directory, with the secret’s
tail trimmed. Send it as Authorization: Bearer ttk_…. mode: users swaps bearers for
argon2id logins and session cookies, and under it an Authorization header is not read at
all. Scoped tta_ management tokens are the other mode: token credential, and they are
the only one a per-operation scope is checked against. Who holds what is
who can administer governance.
One status code is worth knowing before your first call. Where governance is not armed, most
reads answer 200 with an empty view and the writes answer 404 feature_absent with an
enable_hint naming the config change. That 404 means this deployment has no governance
plane, not that your object is missing.
Two reads answer it as well, because neither has an empty view to give: GET /keys/{kid}
and POST /preview. The two exports answer it on a different condition, since they read the
decision ledger rather than the governance plane, and a ledger that has not been written yet
is a 404 too.
The endpoints, by job
Twenty-five operations, on twenty-three paths, counted from the shipped spec. Paths below
drop the shared /tt/api/v1/governance prefix. The role column is the route floor every
credential clears first. A scope printed after the role is checked against a tta_
management token only; a session and a ttk_ bearer take their grant from their role and
face no scope check.
Keys and identity. The ttv_ lifecycle, minus the grace window.
| Operation | What it does | Role |
|---|---|---|
GET /keys |
key metadata for a subtree; no field can hold a secret, and last_used has no writer, so it is always absent |
viewer |
POST /keys |
mint a key; the secret appears once, in this response | admin · keys:mint |
GET /keys/{kid} |
one key by id; an out-of-subtree kid gets the same 404 as an unknown one | viewer |
POST /keys/{kid}/revoke |
switch a key off; the row survives as state: revoked |
operator |
POST /keys/{kid}/rotate |
mint a successor and record the lineage | admin · keys:mint |
Tenancy nodes. The org/team/user tree, as flat parent-pointed rows.
| Operation | What it does | Role |
|---|---|---|
GET /nodes |
the runtime-plane nodes, each with its parent, kind and label |
viewer |
POST /nodes |
create a runtime node of kind org, team or user; the tenancy tree picks it up at the next reload |
operator |
DELETE /nodes/{id} |
delete a node; its overrides cascade away with it | operator |
Limits and overrides. What is armed, and what one node may have instead.
| Operation | What it does | Role |
|---|---|---|
GET /limits |
the armed definitions, plus every persisted overrides row |
viewer |
POST /limits/overrides |
set one node’s amount for one limit | operator |
DELETE /limits/overrides/{limit_id}/{node} |
drop that node’s override | operator |
Status and health. Where a limit stands, and whether enforcement is intact.
| Operation | What it does | Role |
|---|---|---|
GET /status |
per node and per limit: burn, fraction, forecast, overrun bound | viewer |
GET /health |
shared-state reachability, breaker state, suspensions, convergence lag | viewer |
GET /health/fleet |
the same picture per replica, on the federated coverage envelope | viewer |
GET /reconcile |
ledger-recomputed spend drift, as an alert with no adopt button; no rollup can be imported here, so each scope reports its whole burn | viewer |
Refusals and exports. The audit trail, live and in bulk.
| Operation | What it does | Role |
|---|---|---|
GET /refusals |
the curated feed, newest first, cursor- or offset-paged inside a subtree | viewer |
GET /exports/refusals |
the same feed uncapped, JSON or CSV via format=, provenance-stamped |
viewer |
GET /exports/usage |
periodized per-node spend and usage rollup, JSON or CSV | viewer |
Preview and shadow. Two reads that show effect without enforcement.
| Operation | What it does | Role |
|---|---|---|
POST /preview |
dry-run a proposed limit set over ledger history and live counters | viewer |
GET /shadow |
would-refuse counts per mode: shadow limit, with rows observed |
viewer |
Delegated self-serve. A team admin acting inside one subtree.
| Operation | What it does | Role |
|---|---|---|
GET /selfserve |
the caller’s own subtree: nodes, status, keys, refusals | viewer + subtree binding |
POST /selfserve/keys |
mint a key whose owner node is inside that subtree | viewer + subtree binding · keys:mint |
POST /selfserve/overrides |
raise a node’s cap under the template’s declared ceiling | viewer + subtree binding |
Tenant tokens. A read credential you can hand to the tenant.
| Operation | What it does | Role |
|---|---|---|
POST /tenant-tokens |
mint a tts_ token frozen to one subtree, shown once |
admin · keys:mint |
GET /tenant/{token} |
read that one subtree, and nothing above or beside it | none, the token is the credential |
What a schema cannot tell you
Five behaviours decide whether your integration works, and not one of them is a field or a status code you can read off a schema. They are the reason this page exists.
An override is durable now and enforced later. Writing one persists and audits it
immediately, and changes nothing about the limits being applied. Every success comes back
with pending_reload: true, and GET /limits keeps the two apart: definitions is what is
armed, overrides is what is waiting for the next governance reload. The response says so
in its own note:
definitions are the ARMED limits enforced now; overrides are persisted runtime writes that take effect on the NEXT governance reload (compile-bound). No override is reported as live-enforced here.
So after you raise org_monthly_budget from $1,000 to $2,000, definitions keeps reporting
amount.usd: 1000 until governance re-arms. That is the enforced number, not a stale read.
No governance route triggers that re-arm. It happens on a restart, or on a config reload
that changed governance.limits or governance.tenants, which over HTTP means
POST /tt/api/v1/config/apply. A reload touching neither block leaves your override
pending. POST /nodes sits on the same clock: the row lands in governance.db at once and
in the tenancy tree only at the re-arm. Minting a key is the one runtime write that takes
hold immediately.
Two ways the override route itself bites. An unknown limit_id is accepted here with a
200 and pending_reload: true and is then dropped at the reload, while the delegated twin
answers 400 for the same body. And a type that cannot carry a per-node amount (rpm,
tpm, concurrency) is turned away 422 unsupported_override_class. Check the id against
definitions before you trust the 200. Which types take an override, and why, is
the six limit types.
Rotate has no grace window on this API. POST /keys/{kid}/rotate always rotates with
grace 0. The successor is minted and the predecessor goes dark in the same transaction, so
a caller still holding the old secret starts getting 401s straight away. A window is a
command-line concern: tokentriage keys rotate <kid> --grace 24h is the only way to get one,
and it is what you want when in-flight callers matter. The mechanism is on
virtual keys.
The self-serve role floor is a pre-filter, not the gate. All three /selfserve routes
sit at a viewer floor, so the role column on its own reads as “a viewer can mint a virtual
key”. It cannot. Each route also carries x-delegation: subtree, and the decision is made
in the handler: the caller has to hold an admin-authored role binding whose root contains
the requested node, and an unbound viewer clears the floor and then gets a 403 with the
attempt audited under its own username. No route here writes that binding, and no CLI verb
does either; it is a line an admin hand-edits into role-bindings.json under
dashboard.state_dir.
Two more things the matrix cannot show you. Bindings resolve from a username, so they work
only in dashboard.auth.mode: users, which is also the mode that reads no bearer at all: the
keys:mint scope printed on the mint row cannot be what turns a caller away. And the subtree
confines the three /selfserve routes rather than the account, so the same person still
reads every tenant on the routes above. Read
delegated self-serve before you grant anyone the role.
GET /nodes enumerates the runtime plane only. The handler lists the nodes table in
governance.db, whose only writers are POST /nodes and the demo seeder. Tenants declared
under governance.tenants in your config compile straight into the tenancy tree without
ever landing in that table, so a config-only deployment gets {"nodes": []} back while
resolution walks its tree perfectly well. On a sandbox whose governance.tenants declared
five nodes, that empty list is what came back. Both planes merge into one tree when
governance compiles, and this listing shows one of them. What the tree does at request
time is the tenancy hierarchy.
posture collapses three configurations into one word. The field takes alert_only,
enforcing, fail_closed or suspended, and alert_only covers mode: shadow,
enforce: soft and mode: off alike, so a limit watching your traffic looks the same as one
switched off. GET /shadow picks out one of the three, because only mode: shadow writes
the would_refuse marker it counts; a limit at enforce: soft and one at mode: off stay
indistinguishable on both surfaces. What each mode does is
how a limit counts.
Why /tt/v1/me is not part of this API
“What am I allowed to do, and how much is left?” is a tenant’s question, not an operator’s,
and it is answered on the other port. GET /tt/v1/me sits on the data-plane listener
(server.listen, default 127.0.0.1:8787), takes a ttv_ virtual key rather than a
dashboard token, and returns the caller’s own node, chain, scopes and live counters. Nothing
about anyone else.
The split is about which credential you are willing to hand out. A dashboard token is an
operator credential, and at the viewer floor it reads every tenant’s keys, statuses and
spend unless you narrow it with a tts_ tenant token. A ttv_ key is already in that
tenant’s hands, and the route it unlocks takes no parameter that could name another node, so
it has no way to widen. An unrecognised or absent credential gets a bare 401:
$ curl -i http://127.0.0.1:8790/tt/v1/me
HTTP/1.1 401 Unauthorized
Content-Length: 0
Port 8790 is that sandbox’s data-plane listener; yours is whatever server.listen says.
The empty body is the point: the response cannot confirm or deny that any particular key
exists. The full 200 shape, with the resolution chain and per-limit remaining, is on
virtual keys.
What this page does not cover
Field-level detail (every parameter, schema, status code and problem type) is the
rendered API reference, where these operations appear on their own and inside the
Management API document under the governance tag. Command-line equivalents, including the
rotation flag this API does not expose, are in the CLI reference. What
a governance 429 or 403 carries on the wire is the
refusal wire reference. Health, suspensions,
exports and reconcile as a routine are
operating governance.