# The management API

Drive governance over HTTP with the endpoints grouped by job, the role each one needs, and the behaviours no schema records.

Source: https://tokentriage.com/docs/build/governance-api

---
import NextSteps from '../../../components/NextSteps.astro';

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](/api) 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](/docs/build/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:

```console
$ 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](/docs/build/governance-access-control).

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](/docs/build/governance-limits).

**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](/docs/build/governance-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](/docs/build/governance-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](/docs/build/governance-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](/docs/build/governance-enforcement-classes).

## 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:

```console
$ 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](/docs/build/governance-keys#what-a-key-holder-can-see-about-itself).

## What this page does not cover

Field-level detail (every parameter, schema, status code and problem type) is the
[rendered API reference](/api), 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](/docs/reference/cli). What
a governance `429` or `403` carries on the wire is the
[refusal wire reference](/docs/build/governance-refusal-reference). Health, suspensions,
exports and reconcile as a routine are
[operating governance](/docs/build/governance-operations).

<NextSteps items={[
  { title: 'Delegated self-serve', href: '/docs/build/governance-self-serve', description: 'Bind a team admin to a subtree, and what a bound account can still read outside it.' },
  { title: 'Who can administer governance', href: '/docs/build/governance-access-control', description: 'The role ladder, dashboard users and tokens, and what each role can read.' },
  { title: 'Operating governance', href: '/docs/build/governance-operations', description: 'Health, suspensions, alerts, exports and reconcile as a routine.' },
  { title: 'CLI reference', href: '/docs/reference/cli', description: 'The command-line equivalents, including the rotation grace window.' }
]} />
