> ## Documentation Index
> Fetch the complete documentation index at: https://checkfu.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Attribute usage per end user

> Assert a Principal, read raw or daily usage, and cap spend per user.

If you resell agent capability, you need to know which of your users consumed what. Checkfu attributes every Run to a **Principal** (your end user) and records immutable usage rows against it.

## Assert the Principal

Your backend authenticates its own users and asserts the Principal when it calls Checkfu. Checkfu API keys stay server-side and are never exposed to an end user.

`CHECKFU_API_KEY`, `CHECKFU_WORKSPACE_ID`, `CHECKFU_AGENT_ID`, and `CHECKFU_PRINCIPAL_ID` come from [Get access](/reference/access#the-three-variables-ready) and the Agent you published. The TypeScript tabs below use the [TypeScript SDK](/reference/typescript-sdk) with this client:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Checkfu } from "@checkfu/sdk"

const checkfu = new Checkfu({
  apiKey: process.env.CHECKFU_API_KEY,
})
```

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --request POST https://api.checkfu.com/v1/sessions \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-27" \
    --header "Content-Type: application/json" \
    --data '{
      "agent": "'"$CHECKFU_AGENT_ID"'",
      "principal": "'"$CHECKFU_PRINCIPAL_ID"'"
    }'
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const session = await checkfu.sessions.create({
    agent: agentId,
    principal: principalId,
  })
  ```
</CodeGroup>

Every Run records its Principal, so capability access, audit records, and usage all attribute consistently to the same identity.

## Read the ledger

`GET /v1/usage` returns one row per immutable billing fact. Use it when you need exact source rows or Principal, Agent, Session, and Run filters.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --get "https://api.checkfu.com/v1/usage" \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-27" \
    --data-urlencode "principal_id=$CHECKFU_PRINCIPAL_ID" \
    --data-urlencode "limit=100"
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const page = await checkfu.usage.list({ principal_id: principalId, limit: 100 })

  for await (const row of page) {
    // The client follows the next_page cursor for you.
  }
  ```
</CodeGroup>

Filter by `principal_id`, `agent_id`, `surface_scope_id`, `automation_id`, `session_id`, or `run`, and order with `order=asc|desc`. Page with the opaque `next_page` cursor.

Each row carries exactly one measurement dimension:

| `measurement.kind`   | What it measures                                             | Cost field                                      |
| -------------------- | ------------------------------------------------------------ | ----------------------------------------------- |
| `model`              | Token counts, including cached and reasoning tokens          | `tokens`                                        |
| `session_runtime`    | Milliseconds a Run (or SessionThread Run) was `running`      | `running_ms` / `billed_microusd` at \$0.08/hour |
| `sandbox_compute`    | Active sandbox milliseconds (recorded, not invoiced)         | `active_ms`                                     |
| `snapshot_retention` | Retained snapshot storage over time (recorded, not invoiced) | `storage_mib_ms`                                |

`total_tokens` is authoritative. Do not rebuild it from message text or traces. Cached and reasoning tokens are not visible there, and the ledger is the billing record.

<Note>
  **Harness-build compute is not in this ledger and not on this API.** A [HarnessBuild](/concepts/harnesses-and-models) has no Session, Run, or RunAttempt to anchor a `UsageEntry` to, so its compute is metered in a separate build-usage ledger keyed by build and attempt, and the two join only at billing. That means these rows are a complete record of *agent execution* and not of every billable minute — do not reconcile an invoice against them alone. Budgets see the same boundary: a Budget bounds agent execution under a principal-scoped lineage, and a build has none, so builds are bounded by admission limits instead of by your Budget.
</Note>

<Note>
  Every row has exactly one of `principal_id` or `anonymous_subject_ref`. After a Principal is erased under a data-deletion request, `principal_id` goes null while the accounting fact survives. Aggregations must tolerate that, or a deletion will silently change historical totals.
</Note>

## Read daily usage and known spend

`GET /v1/usage/series` returns server-owned Workspace totals in aligned UTC-day buckets. The range is inclusive at `from`, exclusive at `to`, and may span at most 90 days. `bucket=day` is the only bucket, and an IANA display timezone is required; the timezone is echoed for presentation and never moves facts between canonical UTC buckets.

`group_by` takes `none` (Workspace totals) or exactly one attribution dimension — `agent_definition`, `model_routing_profile`, `cost_class`, or `automation`. One dimension, never a cube: two dimensions at once is not a supported read. A grouped read carries no latency guarantee, while `group_by=none` keeps the measured one, so reach for a dimension when you need the breakdown rather than by default.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --get "https://api.checkfu.com/v1/usage/series" \
  --header "Authorization: Bearer $CHECKFU_API_KEY" \
  --header "Checkfu-Version: 2026-08-27" \
  --data-urlencode "from=2026-07-01T00:00:00.000Z" \
  --data-urlencode "to=2026-08-01T00:00:00.000Z" \
  --data-urlencode "bucket=day" \
  --data-urlencode "monetary_view=event_time" \
  --data-urlencode "group_by=none" \
  --data-urlencode "display_timezone=UTC"
```

Usage and spend are separate arrays. The service merges verified closed rollups with the selected bounded raw 48-hour tail. Spend is authoritative local platform charge and correction data, encoded as a decimal-string `exact_picousd`; it is never inferred from catalog pricing. Choose `event_time` to place corrections with the original work, or `accounting_time` to place each monetary fact when it was recorded.

Treat `watermark.status: partial`, `incomplete: true`, or `spend_status: partial` as **known spend**, not total spend. `byok`, `unpriced`, and `unknown` coverage are not zero. The route fails closed when no verified projection exists and never falls back to an unbounded client-side sum.

## Cap spend per user

A Budget scoped to a Principal enforces a ceiling without any work on your side:

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --request POST https://api.checkfu.com/v1/budgets \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-27" \
    --header "Content-Type: application/json" \
    --data '{
      "scope": { "kind": "principal", "principal_id": "'"$CHECKFU_PRINCIPAL_ID"'" },
      "metric": "model_tokens",
      "window": { "kind": "calendar_month" },
      "soft_threshold": { "limit": 800000, "action": "notify" },
      "hard_threshold": { "limit": 1000000, "breach_action": "block_admission" }
    }'
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  await checkfu.budgets.create({
    scope: { kind: "principal", principal_id: principalId },
    metric: "model_tokens",
    window: { kind: "calendar_month" },
    soft_threshold: { limit: 800000, action: "notify" },
    hard_threshold: { limit: 1000000, breach_action: "block_admission" },
  })
  ```
</CodeGroup>

The soft limit must be strictly below the hard limit.

Both `breach_action` values block new admission once the ceiling is reached. The setting decides only whether a turn already in flight is *additionally* interrupted. `block_admission` leaves the running turn alone; `interrupt_turn` stops it.

When a hard threshold blocks a request you receive `429 budget.exceeded`. That error also covers Workspace quotas, ToolInvocation ceilings, and API-key rate limits, so surface it to your user as "limit reached" rather than assuming which limit it was.

Set `metric` to `model_spend` to express both thresholds in integer micro-USD instead. Checkfu converts normalized token dimensions with its reviewed, effective-dated price catalog; provider-reported money is reconciliation evidence only. Attaching the Budget fails with the unpriced provider/model named when any admitted candidate lacks catalog coverage, and the actual candidate set is checked again before every provider call. `session_runtime_ms` caps closed running intervals. `total_spend` caps invoice micro-USD (session runtime plus platform-supply model charges).

<Warning>
  Sandbox compute and snapshot retention appear in the usage ledger but are not invoiced and cannot yet be capped by a Budget, so a compute-heavy workload still needs its own monitoring.
</Warning>

## Separating your limits from Checkfu's

Four independent gates can each refuse work, and none substitutes for another:

| Gate                    | Question                                                                                   | Error                             |
| ----------------------- | ------------------------------------------------------------------------------------------ | --------------------------------- |
| Budget                  | Has this Workspace, Agent, Principal, SurfaceScope, or Automation crossed a limit you set? | `budget.exceeded`                 |
| Entitlement             | May this Organization consume paid Checkfu capability?                                     | `billing.usage_limit_reached`     |
| Payment                 | Is the Organization's account in good standing?                                            | `billing.payment_required`        |
| Platform supply ceiling | Is Checkfu's own provider capacity available?                                              | `model.platform_capacity_reached` |

All applicable gates must allow a request. Distinguish them in your UI: the first is your own policy and you can raise it, the middle two need action from an Organization administrator, and the last clears on its own (or permanently, by [bringing your own key](/concepts/harnesses-and-models#model-routing-profiles)).

## Next steps

<CardGroup cols={2}>
  <Card title="Automations and operations" icon="clock" href="/concepts/automations">
    The full Budget and Usage shapes.
  </Card>

  <Card title="Tenancy and governance" icon="shield" href="/concepts/tenancy-and-governance">
    How Principals relate to Workspaces and PermissionAssignments.
  </Card>
</CardGroup>
