# Read the agent-usage ledger

`GET /api/v1/usage/ledger` · operationId `getUsageLedger`

For developers: your server sends this request with a server key; it is not a button in the dashboard. To see the same information without code, open **Charge › Usage (Ledger)** in the dashboard.

Completed-work units (POST /api/v1/usage) counted as chargeable agent (high confidence: verified identity, an automation trace, injected input, sustained behaviour, a behaviour change or two or more behaviour families agreeing), behaviour-only agent (medium confidence, never charged), human or unknown, per Site, account, user and UTC day. Append-only: later changes are adjustment entries; erasure removes identity, never counts. Each entry carries its evidence (session IDs, verdict basis, how we know) and an idempotencyKey. No charges are created.

## Authentication

Send `Authorization: Bearer YOUR_SERVER_KEY` (siteServerKey: Site server key in Authorization: Bearer <key> (the scheme is case-insensitive and required; a bare key is 401). Shown once on creation or rotation. Publishable keys cannot authenticate). Keep the server key in server-only configuration; never send it to a browser.

## Parameters

- `view` (query, "days" | "accounts" | "entries", optional): days: per UTC day, account and user; accounts: totals per account; entries: the entries with their evidence, oldest first.
- `from` (query, string, optional): Period start: Unix epoch ms or ISO 8601 date. Defaults to the last 30 whole UTC days. Clamped to 400 days for counts and to the Site usage retention for entries.
- `to` (query, string, optional): Period end (exclusive).
- `accountId` (query, string, optional)
- `userId` (query, string, optional)
- `usageId` (query, string, optional): entries view: one unit.
- `limit` (query, integer, optional)
- `cursor` (query, string, optional): page.nextCursor from the previous response; it keeps that view, period and filters.

## Responses

- **200**: Agent-usage ledger · body `UsageLedger`
- **400**: bad_request: the request is invalid; message says what to fix and field names the request field · body `Error`
- **401**: unauthorized (missing or invalid server key, missing Bearer scheme) · body `Error`
- **403**: forbidden: the call came from a browser page or another Site; call from your server with this Site’s server key · body `Error`
- **404**: not_found: nothing with this ID on this Site (or it is past retention), or an unknown path · body `Error`
- **429**: rate_limited: wait for Retry-After · body `Error` · headers `Retry-After`
- **500**: internal_error: fail the protected action; do not retry automatically · body `Error`
- **503**: service_unavailable: temporarily unavailable; only verify retries · body `Error`

### Success body fields

- `object` ("usage_ledger", required)
- `siteId` (string, required)
- `view` ("days" | "accounts" | "entries", required)
- `since` (number, required): Unix epoch milliseconds.
- `until` (number, required): Unix epoch milliseconds.
- `period` (object, required)
- `period.from` (string, required)
- `period.to` (string, required)
- `period.halfOpen` (true, required)
- `clamped` (boolean, optional): true when from was moved forward to what the Site still keeps.
- `coverage` (object, required)
- `coverage.countsFrom` (string, required): Oldest UTC day with day counts (YYYY-MM-DD).
- `coverage.entriesFrom` (string, required): Oldest entry still kept (the Site usage retention).
- `classes` ("billable" | "behaviour" | "human" | "unknown"[], required)
- `note` (string, optional)
- `totals` (UsageLedgerCounts, optional): days and accounts views: the whole period, all pages.
- `days` (UsageLedgerCounts[], optional): days view: one row per UTC day, account and user.
- `accounts` (UsageLedgerCounts[], optional): accounts view: one row per account, most chargeable units first.
- `entries` (object[], optional): entries view: the entries, oldest first.
- `entries[].id` (string, required)
- `entries[].type` ("unit" | "adjustment" | "route_call" | "erasure", required): unit: one POST /api/v1/usage record. adjustment: a later correction (class or units changed; the original stays). route_call: an approved-route call (MCP, API or page tool). erasure: a subject was erased (no identity).
- `entries[].usageId` (string | null, optional)
- `entries[].at` (number | null, optional)
- `entries[].postedAt` (number, required): Unix epoch milliseconds.
- `entries[].day` (string, required)
- `entries[].accountId` (string | null, optional)
- `entries[].userId` (string | null, optional)
- `entries[].actionId` (string | null, optional)
- `entries[].units` (integer, required)
- `entries[].class` ("billable" | "behaviour" | "human" | "unknown" | null, optional)
- `entries[].fromClass` (string, optional): adjustment only: the class before.
- `entries[].reason` (string, optional)
- `entries[].counts` (object, required): Units per class for this entry (an adjustment is negative in its old class).
- `entries[].evidence` (object | null, optional): Session IDs, the verdict basis, evidenceClass (verified-identity, automation-trace, injected-input, sustained-behaviour, behaviour-change, behaviour-corroborated, cursor-driven, behaviour-only, human, unknown) and classLabel.
- `entries[].idempotencyKey` (string, required): tt_ plus the entry ID: use it as the idempotency key when you forward the entry to your billing system.
- `page` (object, required)
- `page.limit` (integer, required)
- `page.total` (integer, optional): days and accounts views only.
- `page.nextCursor` (string | null, required): Send as cursor to read the next page; null on the last page.

Failures are not verdicts. Error bodies follow the `Error` schema: `code` is one of `bad_request`, `unauthorized`, `session_reinit_required`, `forbidden`, `not_found`, `conflict`, `identity_required`, `payload_too_large`, `rate_limited`, `internal_error`, `service_unavailable`, `request_failed`, `approval_invalid`, `approval_used`, `approval_expired`, `approval_mismatch`, `step_up_not_held`, `step_up_mismatch`, `step_up_settled`, `step_up_expired`, `step_up_superseded`, `agent_tokens_disabled`, `tool_not_approved`, `tool_not_read_only`. [Errors and retries](/docs/errors) explains each code and what to do.

## Example

```sh tab=curl
curl -sS -X GET 'https://app.trytelltale.dev/api/v1/usage/ledger' \
  -H "Authorization: Bearer $TELLTALE_SERVER_KEY"
```

```js tab=fetch
const response = await fetch("https://app.trytelltale.dev/api/v1/usage/ledger", {
  method: "GET",
  headers: {
    Authorization: 'Bearer ' + process.env.TELLTALE_SERVER_KEY
  },
  signal: AbortSignal.timeout(5000)
});
const result = await response.json();
```

Machine-readable contract: [openapi.json](https://app.trytelltale.dev/openapi.json). Request builder: the [API explorer](/docs/api-explorer) shows every operation as curl; it sends requests only on a local dashboard.
