# Usage metering

Telltale records units of completed work that your backend reports, and separately measures agent tool executions and agent session-minutes per account and category. It meters; it does not create charges or process payments. Pass the numbers to your own billing system.

## Report completed work

After the protected work has actually completed, report it from your backend with the server key:

```sh
curl -sS -X POST https://app.trytelltale.dev/api/v1/usage \
  -H "Authorization: Bearer $TELLTALE_SERVER_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"sessionToken":"TOKEN","eventId":"export_000001","actionId":"export_report","units":1}'
```

With the server adapter: await telltale.recordUsage({ sessionToken, eventId, actionId, units }).

Optionally include `executionId`, the stored execution ID, when your backend has an explicit correlation to a tool execution. The reviewed runner exposes this ID as the third `beforeRequest(request, action, context)` argument’s `context.executionId`; your application can deliberately forward it to its own backend. Treat it as correlation only, never authorization or proof of success. Do not guess from the nearest timestamp. The collector requires the same site, session, trusted account and action name. The same `eventId` must always repeat the same execution link. Existing callers can omit it.

For a multi-step execution, the matching name is the **workflow’s tool name**, not the individual step action name. Configure the allowed operation-to-tool mapping in your backend; do not blindly trust a browser-supplied name. A successful last-step request alone is not proof that the whole workflow’s business outcome completed. If your backend can confirm only that individual operation, report it without the workflow execution link rather than claiming completion of the entire workflow.

A linked receipt records `outcomeBasis: application-server-reported` and updates the execution’s application outcome summary. It is your authenticated server’s attestation, not independent proof by Telltale. A browser-reported successful request remains a separate result; browser completion cannot create this receipt. The usage event retains the receipt and link, while the execution keeps its latest receipt and aggregate units. Report only after your application confirms its business action succeeded.

- eventId is your stable 8–128 character identifier (letters, digits, underscore, hyphen) for this piece of work. Derive it from your own record (for example the export ID), not from a timestamp or random value, so a retry carries the same ID.
- units is an integer from 1 to 1,000,000.
- The session must already carry server-verified identity: call POST /api/v1/identify after your own authentication first. Usage on a session without it is refused with 409 identity_required: identify the session, then send the same request again with the same eventId.
- Resending an eventId with identical facts returns the original record with replayed: true, so retries are safe. The eventId is checked before the session token, so a retry still gets the original record after the browser session expired (24 hours), was withdrawn or was removed. Reusing an eventId with different facts is rejected with 409 conflict: reconcile the original facts before retrying. Use a new eventId only for genuinely separate completed work, never to bypass a conflict for the same work.
- New work needs a live session token: report it within the session's 24-hour life. Work that finishes after the session ended cannot be reported against that session; report it when it completes, not in a later batch.
- Report only completed work. Verification checks and tool starts are not chargeable completions. Usage calls are not retried automatically by the adapter; retry them yourself with the same eventId.

Each recorded unit also emits a signed usage.recorded webhook ([Webhooks guide](/docs/webhooks)).

### Recover after receipt delivery fails

Keep the business result separate from receipt delivery. If an export was saved but `recordUsage` failed, show “Report saved; usage reporting pending” rather than “Export failed.” Retry only the receipt with the same eventId and facts; do not repeat the export. A `409 conflict` is not a transient delivery failure: show “Work completed; usage reporting needs review” and reconcile the action name, execution link and original event facts before retrying. Keep it out of automatic delivery retries. Persist the completion and pending receipt in your own backend so recovery does not depend on an open browser or an in-memory queue. A timeout can mean the collector accepted the receipt but its response was lost, so use the same ID even when the outcome is uncertain. The 24-hour live-session requirement still applies to a receipt the collector has not accepted yet; an expired-session rejection needs reconciliation, not a new event ID or an invented session.

Each row also records via, how the work reached your application, derived when it arrives: tool when a reviewed tool or workflow with the same name as actionId ran in that session within the 15 minutes before; ui when no tool ran in that window and the session sent UI interactions; unknown otherwise (another tool ran, or no interaction was observed). unknown is never counted as ui, and via says nothing about who drove it; the verdict does. Control › Tool access › Reviewed shows, per tool, agent-completed work through the tool versus agents driving the UI (tool coverage), plus tool runs by agent and by account. Charge › Usage groups reports by week, action and account.

## What counts as chargeable

Only high-confidence agent usage is chargeable: verified identity (a valid Web Bot Auth signature, `agentEvidence: "verified"`), an automation trace (`"automation-trace"`), input injected by software (`"injected-input"`), sustained behaviour (`"sustained-behaviour"`), a behaviour change after a person (`"behaviour-change"`), two or more independent behaviour families agreeing on one page with no physical-device input on the session (`"behaviour-corroborated"`, "Strong evidence: several behaviour signals agree"; ledger evidence class `behaviour-corroborated`) or Claude in Chrome's cursor driving the clicks (`agentEvidence: "cursor-driven"`; ledger evidence class `cursor-driven`). High confidence is chargeable from the minute it qualifies; earlier medium-confidence minutes of the same session are not charged. Agent minutes and completed work at medium confidence (one behaviour pattern alone, `agentEvidence: "behaviour"`) are recorded and shown, marked `billable: false` (`billable` is shown as chargeable in the dashboard), and never metered, included or priced, whatever the Site's blocking setting. Agent tool executions come through the agent tool surface and always count.

- In GET /api/v1/agent-usage.json and agent_usage.summary, `toolExecutions` and `sessionMinutes` still count every agent unit shown. `metered` and `included` count chargeable units only. New: `billable` (metered plus included) and `behaviourOnly` (shown, never charged) on totals, categories, accounts and agents (`billableUnits` and `behaviourOnlyUnits` in agent_usage.summary). `billable` plus `behaviourOnly` always equals the unit totals. series and previous carry the same split.
- agent_usage.recorded carries `billable` and `agentEvidence`; `meteredUnits` is 0 for a behaviour-only unit. If stronger evidence (including sustained behaviour qualifying) arrives in the same session-minute, the one unit is revised to chargeable: agent_usage.recorded is sent again with the same usageId, `revised: true` and `previouslyBillable: false`. It is never a second unit.
- usage.recorded and the usage rows from POST /api/v1/usage carry `agentEvidence` and `billable` (true only for agent work that is not behaviour-only). The pricing estimate never prices behaviour-only minutes, seats or work, and lists them apart (`behaviourOnlyAgentMinutes`, `workNotPriced.behaviourOnly`).
- Rows recorded before heuristic-v4.26 have no `billable` field and count as they did.

## Agent usage by category

GET /api/v1/agent-usage.json returns seven days of retained agent tool executions and agent session-minutes per account and category: computer_use, browser_use, coding_agent, crawler or unattributed. Pass from and to for another period. Periods are half-open, [from, to): a unit exactly at to belongs to the next period, so consecutive periods add up without double counting.

- A named category needs an agent name that is Verified (`signed`) or Unconfirmed (`weak-marker`), or a verified crawler identity. Generic automation always lands in `unattributed` (shown as Unnamed agents); it never establishes which agent or AI model was driving.
- The category and account are captured when the unit is ingested and never reassigned. Metered or included follows the category's billing mode now: changing it applies to every retained unit of that category, whenever it arrived, so the API, the dashboard's Charge › Usage and its pricing estimate count units by one rule. Each unit still records the mode in force when it arrived (billingMode, also on its agent_usage.recorded webhook).
- Each unit emits a signed agent_usage.recorded webhook with usageId, unit, accountId, accountSource, category, billingMode and meteredUnits. Owners and admins can also send an agent_usage.summary webhook from the dashboard. It covers the default seven-day period and carries since, until, periodBounds ([since, until)), asOf, coverageStart, oldestRetainedAt and totals with the same values as GET /api/v1/agent-usage.json for that period, plus per-account and per-category metered and included units.
- Usage by named agent: agent-usage.json and agent_usage.summary also carry agentsUsed (distinct named controllers in the period) and agents, one row per controller and attribution strength with controller, strength (signed, weak-marker, ip-verified for a crawler or fetcher verified against its operator's IP list, or unattributed), category, units and lastAt. An agent is named only from Verified (`signed`) or Unconfirmed (`weak-marker`) attribution captured at ingestion; everything else is one unnamed-agent row whose controller is null, never a guessed name. Names identify a controller or operator, not an AI model.

## Agent-usage ledger

Every completed-work unit also lands in the [agent-usage ledger](/docs/usage-ledger): chargeable, behaviour-only, human and unknown units per account, user and day, with the evidence for each unit, corrections, a Stripe meters export and account alerts.

## Reconcile

Use eventId (completed work) and usageId (agent units) as idempotency keys in the system you charge your customers with, and webhook-id to deduplicate deliveries. Periodically compare your ledger with GET /api/v1/agent-usage.json for the retained window, using consecutive half-open periods. Usage, agent minutes and agent tool executions are retained for 90 days; record caps never remove them early, only age or erasure does. Import continuously rather than relying on the API as the record of truth.

Charge › Usage shows the same data and exports CSV (GET /api/admin/export?site=SITE_ID, owner session):

- By default the export holds chargeable completed work only (kind work_completed). Pass kind=verification, kind=tool_started or kind=all for the non-chargeable rows.
- Columns: id, created, sessionId, accountId, userId, kind, actionId, units, verdict, basis, source, accountResponse (the account response snapshotted when the row was recorded), via (tool, ui or unknown, completed work only), createdAt (ISO 8601 UTC), executionId, outcomeBasis, agentEvidence and chargeable (yes or no for agent rows).
- from and to (Unix epoch milliseconds or ISO 8601 dates) limit the export to the half-open period [from, to); without them it holds the whole retained history.
- type=agent-minutes exports agent session-minutes instead: id, at, sessionId, accountId, accountSource, userId, agentCategory, billingMode (when the minute arrived), billingModeNow (the category's billing now, which Charge › Usage counts by), accountResponse, observedVerdict, atIso (ISO 8601 UTC), agentEvidence and chargeable (yes or no). Event values are never exported.

## Trends and the leadership report

- series=day|week and compare=previous on GET /api/v1/agent-usage.json add buckets that add up to the totals and the previous period of the same length. GET /api/v1/activity.json gives human, agent, unknown and mixed interaction minutes and session totals per UTC day for BI tools.
- The dashboard's Home › More numbers shows agent, human and unknown shares of sessions and of activity minutes (interaction minutes) with the change against the previous period in percentage points, agent minutes and the share with a named agent, completed work by channel, tool runs, top accounts, pages and tools, a plan breakdown and 12 weeks. Daily counts by verdict at last activity (last-observed verdict) are kept 400 days as counts only (sessions themselves are kept 8 days); session shares start the day this version first ran.
- Your agent report › Download (a month or another period) downloads a self-contained HTML report (inline CSS and SVG, no scripts, a print stylesheet, no session or user IDs) and a CSV for the period or a date range (GET /api/admin/report.html and report.csv with days or from/to, to exclusive). Sharing is by file; there is no public link and the dashboard stays on loopback.
- The optional weekly agent report, a report.weekly webhook (POST /api/admin/report-schedule {siteId, weekly}) sends the last complete ISO week's summary numbers to your own endpoints, signed like every webhook. It carries no accounts, pages, tools, sessions or users.

## Pricing estimate

Charge › Pricing simulator has a pricing estimate (POST /api/admin/pricing-preview): unit prices per agent category minute, per agent tool run and per agent-completed action unit, plus an included allowance per account, applied to a past period. It shows what each account would have paid, labelled Estimate, no charges. Nothing is charged, invoiced or stored. Unnamed agents (`unattributed`) have their own price and their own line (and its share of agent minutes), signed-out traffic and accounts from the page (unconfirmed) are shown but excluded from the total, completed work is priced only when its verdict was agent, and by default only categories set to metered are priced.

## Goals and alerts

Settings › Alert rules (GET and POST /api/admin/goals) watches agent share of activity minutes for the site or one account, a named agent seen for the first time, a tool failure rate, or an account whose agent calls keep failing (an account's MCP server and API calls failing at or above your share in one UTC day, with at least your minimum of calls; once per account per day, naming the account in the bell, the account.agent_calls_failing webhook event and, if you choose, Slack). A crossing raises a dashboard banner until dismissed and sends a signed alert.threshold_crossed webhook. Goals use observed counts: the agent share denominator includes unknown and mixed minutes, and unknown never counts toward human.

## Activation and session volume

Each site records when it was **activated**: the moment its first session arrived. Sessions are counted when the SDK starts a new one (a page that resumes a stored session is not counted again), in one counter per site and UTC day that is kept for 90 days. Session records themselves are kept for only 8 days, so the counter is what makes a 30-day total possible.

Settings › Channels shows the activation time, the sessions in the last 30 UTC days (today included) and the first counted day. Sites that had traffic before activation was recorded show their earliest retained session instead, labelled as such. The same numbers are in GET /api/admin/state under siteUsage, keyed by site ID: activated, activatedAt, activationSource (recorded or earliest-retained-session), windowDays, since, sessions30d, countingSince, quota, quotaState, quotaUsedPercent and enforcement.

An owner or admin can set an optional **soft quota** in sessions per 30 days (POST /api/admin/session-quota with siteId and quota, a whole number from 1 to 1,000,000,000, or null to remove it). The dashboard asks for confirmation, and each change is written to the audit log. quotaState is none, ok, near (80% or more) or over. The quota only warns: enforcement is always none, and collection, verification and metering continue past it.

## Erasure

Owners and admins can delete one user's or account's local records from Settings › Data & privacy (POST /api/admin/erase with siteId and userId, accountId or both; values are trimmed). It removes the matching sessions, executions, usage, agent minutes and interaction minutes, including rows recorded before identity was associated and rows whose session has already expired. Erasing an accountId also clears that account's response setting, its name and plan (accountTraits) and goals or alerts scoped to it, and removes its ID from seat-change audit entries. Daily session totals hold counts only and keep a count already rolled into a day. Completed-work usage leaves an identity-free tombstone (record ID and a hash of the facts) until the row would have expired, so a retried eventId is still answered as a replay and is never recorded or charged twice.
