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· headersRetry-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 explains each code and what to do.
Example
curl -sS -X GET 'https://app.trytelltale.dev/api/v1/usage/ledger' \
-H "Authorization: Bearer $TELLTALE_SERVER_KEY"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. Request builder: the API explorer shows every operation as curl; it sends requests only on a local dashboard.