# The management API behind the dashboard

Every screen is a client of one HTTP API on the admin plane. What it covers, how it is metered, and where its reference lives.

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

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

The dashboard has no private channel to the engine. Every figure on every screen came from an
HTTP call you can make yourself, against `/tt/api/v1` on the admin listener, documented by an
OpenAPI specification the binary serves.

That matters for two reasons. Anything the interface can do, a script can do. And when a
surface shows you something surprising, you can ask the same question directly and compare.

## The reference

The full specification is rendered at [the API reference](/api): every operation, its
parameters, its response schema, its errors, with request samples. It is generated from the
specification the binary serves rather than written alongside it.

Your own instance serves the same document, which is the copy to trust if the two ever differ:

```console
$ curl -s http://127.0.0.1:9090/tt/api/v1/openapi.yaml
```

That document and the browsable reference at `/tt/api/docs` both sit at the viewer floor by
default. Setting `dashboard.docs_public: true` moves them to the public floor for teams that
want an open reference; the default is `false`.

## What it covers

Roughly the shape of the [surface index](/docs/build/dashboard-surfaces). Reads over the
ledger (`/query`, `/rows`, `/facets`, `/requests/{id}`), and over pricing, providers, cache,
governance, quality and evidence. Writes for configuration, ladder mode, saved views,
schedules, calibration activation, reconciliation imports and pricing overrides.
Administration for users, tokens and share tokens.

Start from `/capabilities`, which needs no credential and answers what this instance can do:
its engine mode, its pricing snapshot, which features are composed, the whole filter-field
vocabulary, and the API's own conventions.

```console
$ curl -s http://127.0.0.1:9090/tt/api/v1/capabilities
{"api_version":2,"engine":{"version":"dev","mode":"ledger","pricing_snapshot":"litellm@2026-07-14#179affb"},"features":{"index":true,"stream":true,"control_plane":true,"replay":false,"budgets":{"defined":0,"enforce_granted":false},"alerts":false,"anomaly":true,...
```

<Callout type="key">
Read `/capabilities` first. It tells you which endpoints this deployment will answer, so a
client can adapt to an instance instead of discovering absence through errors.
</Callout>

## Conventions worth knowing before you write a client

The capability document publishes these too, with the reasoning, so a client can read them
rather than hard-coding them.

**Two rate budgets, per principal.** Reads and writes are metered separately, so a write storm
cannot starve the reads you need during an incident. The defaults are 600 reads and 120 writes
per minute, set by `dashboard.api_limits`. Classification follows what an operation *does*
rather than its HTTP method: several heavy reads travel by `POST` because they carry a filter
body, and they draw the read budget. The response header says which budget it charged.

**A fixed window admits about twice its budget across a boundary.** That is inherent to the
algorithm rather than a defect, and it is the bound to pace against.

**Cursors are opaque.** Echo `next_cursor` back; do not construct one. On the finite feeds its
absence is the end of the feed. The event log is the exception and says so on its own
operation: it has no end, so its cursor is present on every response and means "resume from
here whenever you next poll".

**A limit above the maximum is refused rather than clamped**, with the cap named, because a
silently clamped page would misreport what the server heard. One operation clamps instead and
discloses the clamp on the response.

**Every response carries a request id**, and the same value appears in any error body and on
the server's own log line for that request.

<Details summary="Idempotency, events and webhooks">

**Idempotency** is available on the operations that advertise it, through an `Idempotency-Key`
header. Three properties to design around: a replay returns the stored first response verbatim
including a stored error, the store is per-process so a retry landing on another node
re-executes, and the store is bounded so an eviction before the retention window also causes a
re-execution. Treat the window as a maximum rather than a guarantee.

**The event log** at `/events` is a bounded ring of successful mutations — configuration
applied, mode set, keys minted or revoked, budget thresholds crossed, calibrations activated.
It is derived from the audit trail, which records every attempt including the rejected ones,
so the trail is the underived answer and polling the log is always sufficient.

**Webhooks** deliver those same payloads with a standard signature scheme. Subscriptions are
configuration rather than an API resource, they flow the same validate-diff-apply loop, and
adding one requires a restart. Delivery is at least once: deduplicate on the message id, and
enforce the timestamp tolerance or a captured delivery replays forever.

</Details>

## Authentication and scope

The API uses the same credentials and the same roles as the interface. A session cookie works
if you are already signed in; a bearer token is what a script should use, and a
session-authenticated write additionally needs the CSRF header that a token principal does not.

A token can carry an explicit scope list narrower than its role, which is how a Grafana panel
or a CI job gets exactly the access it needs.
[Roles and permissions](/docs/build/dashboard-roles) has the vocabulary and the privileged set.

Errors are RFC 9457 problem documents with a stable `type` URL, and the instance serves its
own catalogue of them at `/tt/api/v1/problems`. A `403` caused by a missing scope names that
scope in a response header as well as in the body.

## What it is not

**It is not the data plane.** Nothing here proxies an LLM request. Your traffic goes to
`server.listen` and is documented by [connect your
app](/docs/get-started/connect-your-app).

**It is not a metrics endpoint.** `/metrics` on the same listener serves Prometheus, and that
belongs to [Observability](/docs/build/observability-metrics).

**It does not change under you silently.** The capability document carries an API version and
a management-API version, and a deprecated operation carries a `Deprecation` header. A
`Sunset` header appears only once a removal date has been chosen, so its absence means not yet
scheduled rather than an invented date.

<NextSteps items={[
  {title: "API reference", href: "/api", description: "Every operation, schema and error, rendered from the specification the binary serves."},
  {title: "Roles and permissions", href: "/docs/build/dashboard-roles", description: "The scopes a token can carry, and the seven privileged operations."},
  {title: "Sign in to the dashboard", href: "/docs/build/dashboard-auth", description: "Minting the bearer token a client should present."},
  {title: "Surface index", href: "/docs/build/dashboard-surfaces", description: "The screens these endpoints sit behind, and the question each one answers."}
]} />
