# Report metered agent usage per account and category

`GET /api/v1/agent-usage.json` · operationId `getAgentUsage`

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 (Usage)** in the dashboard.

Retained tool executions and agent session-minutes for the requested period (default seven days, at most the Site usage retention). Totals and categories cover the whole period; accounts are paginated. Use for billing reconciliation; signed agent_usage.recorded webhooks carry the same units as they are ingested. 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

- `from` (query, string, optional): Period start: Unix epoch ms or ISO 8601 date. Defaults to the last 7 days: 7 whole UTC days ending today; clamped to the Site usage retention (90 days by default, up to 400 in Settings › Data).
- `to` (query, string, optional): Period end; defaults to now and never passes it.
- `accountId` (query, string, optional)
- `limit` (query, integer, optional): Account rows per page.
- `cursor` (query, string, optional): page.nextCursor from the previous response; it keeps that response period and account.
- `series` (query, "day" | "week", optional): Adds series: UTC-day or ISO-week buckets of the period that add up to totals.
- `compare` (query, "previous", optional): Adds previous (the period of the same length before since) and change.

## Responses

- **200**: Agent usage report · body `AgentUsage`
- **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

Retained seven-day agent usage. Category and account are snapshots taken at ingestion; metered and included follow the current billing mode of each category (mode), for every retained chargeable unit; behaviour-only AI agent units are counted in the unit totals and behaviourOnly, never in metered or included. Unattributed means agent evidence without a named controller. Not an invoice.

- `siteId` (string, required)
- `since` (number, required): Unix epoch milliseconds.
- `until` (number, optional): Unix epoch milliseconds.
- `asOf` (number, required): Unix epoch milliseconds.
- `window` (string, optional): The period in words, for example "Oct 21 – Oct 27 UTC (last 7 days)".
- `windowDays` (number, optional): Length of the requested period in days.
- `accountId` (string, optional): Present when filtered to one account.
- `clampedToRetention` (boolean, optional): true when from was earlier than the Site usage retention (90 days by default, up to 400) and was moved forward.
- `periodBounds` ("[since, until)", optional): The period is half-open: a unit exactly at until belongs to the next period.
- `coverageStart` (number, optional): Earliest instant this report can cover: the later of since and the Site usage retention floor. Chargeable units are never removed by count caps.
- `oldestRetainedAt` (integer | null, optional): Timestamp of the oldest agent unit this Site still retains, in any period; null when none.
- `page` (object, optional)
- `page.limit` (integer, required)
- `page.total` (integer, required): Account rows in the whole period.
- `page.nextCursor` (string | null, required): Continues the same period and account in stable (accountId, source, category) order. Sending it with a different from, to or accountId is rejected with 400.
- `totals` (object, optional): toolExecutions and sessionMinutes count every agent unit shown; metered and included count chargeable units only; billable and behaviourOnly split the units by whether they count toward billing.
- `categoriesUsed` (integer, optional)
- `categories` (object[], required)
- `categories[].category` ("computer_use" | "browser_use" | "coding_agent" | "crawler" | "unattributed", required)
- `categories[].label` (string, required)
- `categories[].mode` ("metered" | "included", required)
- `categories[].toolExecutions` (integer, required)
- `categories[].sessionMinutes` (integer, required)
- `categories[].metered` (AgentUsageUnits, required)
- `categories[].included` (AgentUsageUnits, required)
- `categories[].billable` (AgentUsageUnits, required): Units that count toward billing: metered plus included.
- `categories[].behaviourOnly` (AgentUsageUnits, required): AI agent units whose verdict rests on behaviour alone (no verified identity or automation trace): shown, never charged. billable plus behaviourOnly equals the unit totals.
- `categories[].tools` (integer, required)
- `categories[].accounts` (integer, required)
- `accounts` (object[], required)
- `accounts[].accountId` (string | null, required)
- `accounts[].source` ("server-associated" | "browser-supplied" | "unattributed" | "not-recorded", required)
- `accounts[].category` ("computer_use" | "browser_use" | "coding_agent" | "crawler" | "unattributed", required)
- `accounts[].currentResponse` ("default" | "flagged" | "agent_seat", optional): Current per-account response setting.
- `accounts[].toolExecutions` (integer, required)
- `accounts[].sessionMinutes` (integer, required)
- `accounts[].metered` (AgentUsageUnits, required)
- `accounts[].included` (AgentUsageUnits, required)
- `accounts[].billable` (AgentUsageUnits, optional)
- `accounts[].behaviourOnly` (AgentUsageUnits, optional)
- `accounts[].byResponse` (object, optional): Units by the account response snapshotted at ingestion (default, flagged, agent_seat); older units without a snapshot are not counted here.
- `agentsUsed` (integer, required) · minimum 0: Distinct named controllers in the period.
- `agents` (object[], required): One row per controller and attribution strength. A controller is named only from signed or weak-marker attribution, or a crawler verified against the operator IP-range list (ip-verified), captured at ingestion; all other agent usage is one row with controller null and strength unattributed.
- `agents[].controller` (string | null, required)
- `agents[].strength` ("signed" | "weak-marker" | "ip-verified" | "unattributed", required)
- `agents[].category` ("computer_use" | "browser_use" | "coding_agent" | "crawler" | "unattributed", required)
- `agents[].toolExecutions` (integer, required)
- `agents[].sessionMinutes` (integer, required)
- `agents[].metered` (AgentUsageUnits, required)
- `agents[].included` (AgentUsageUnits, required)
- `agents[].billable` (AgentUsageUnits, required)
- `agents[].behaviourOnly` (AgentUsageUnits, required)
- `agents[].lastAt` (number | null, required)
- `coverage` (string, optional)
- `seriesBucket` (string, optional): Present with series: UTC day or ISO week (Monday 00:00 UTC).
- `series` (object[], optional): Present with series=day|week: half-open buckets [start, end) that partition the period and add up to totals.
- `series[].start` (number, required): Unix epoch milliseconds.
- `series[].end` (number, required): Unix epoch milliseconds.
- `series[].toolExecutions` (integer, required) · minimum 0
- `series[].sessionMinutes` (integer, required) · minimum 0
- `series[].metered` (AgentUsageUnits, required)
- `series[].included` (AgentUsageUnits, required)
- `previous` (object | null, optional): Present with compare=previous: the period of the same length ending at since (clamped to retention), or null when it lies entirely outside retention.
- `previous.since` (number, optional): Unix epoch milliseconds.
- `previous.until` (number, optional): Unix epoch milliseconds.
- `previous.clampedToRetention` (boolean, optional)
- `previous.totals` (object, optional)
- `change` (object, optional): Present with compare=previous when a previous period exists: unit count differences.
- `change.toolExecutions` (integer, optional)
- `change.sessionMinutes` (integer, optional)

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/agent-usage.json' \
  -H "Authorization: Bearer $TELLTALE_SERVER_KEY"
```

```js tab=fetch
const response = await fetch("https://app.trytelltale.dev/api/v1/agent-usage.json", {
  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.
