# GET /v1/usage & /v1/generation

Two read-only gateway endpoints report your spend. `GET /v1/usage` returns your balance and
recent attempts. `GET /v1/generation` returns the cost of one request, attempt by attempt. Both
read the ledger that your balance comes from, so they match your bill.

Both return **metadata only**: token counts, outcomes and costs. They never return prompts or
completions. See the [Data policy](/docs/data-policy).

In the console, open Usage or Logs for the same data across your organization.

## Authentication

Both endpoints need a billed key: the first key shown after verified sign-in, or one created in
the console. Results cover that key's org, workspace and principal. Send the key in either
header:

```bash
curl -s https://demo-api.routerplus.com/v1/usage -H "Authorization: Bearer $TM_API_KEY"
curl -s https://demo-api.routerplus.com/v1/usage -H "x-api-key: $TM_API_KEY"
```

> [!NOTE]
> A static development key (a gateway running without the money plane) has no
> ledger to read. On these routes it gets the same 404 as any unknown path.

## GET /v1/usage

Returns the 20 most recent attempts made with the keys of your workspace and principal, newest
first.

```bash
curl -s https://demo-api.routerplus.com/v1/usage -H "Authorization: Bearer $TM_API_KEY" | jq
```

```json
{
  "balance_usd": 0.98971,
  "credited_usd": 1.0,
  "spent_usd": 0.01029,
  "recent_attempts": [
    {
      "request_id": "0b6f6a4e-6a01-4c5e-9d5a-1c9f2b7e8a33",
      "deployment": "provider-b",
      "model": "claude-sonnet-4-5",
      "outcome": "completed",
      "usage_provenance": "observed",
      "input_tokens": 1200,
      "output_tokens": 350,
      "cost_usd": 0.00669,
      "billing_source": "house",
      "inference_cost_usd": 0.00669,
      "inference_price_source": "platform_list",
      "price_snapshot_id": "5d2c…",
      "fee_policy_version": "house-pass-through-v1",
      "at": "2026-09-04T09:12:33.104Z"
    },
    {
      "request_id": "0b6f6a4e-6a01-4c5e-9d5a-1c9f2b7e8a33",
      "deployment": "provider-a",
      "model": "claude-sonnet-4-5",
      "outcome": "failed",
      "usage_provenance": "observed",
      "input_tokens": 1200,
      "output_tokens": 0,
      "cost_usd": 0.0036,
      "billing_source": "house",
      "inference_cost_usd": 0.0036,
      "inference_price_source": "platform_list",
      "price_snapshot_id": "5d2c…",
      "fee_policy_version": "house-pass-through-v1",
      "at": "2026-09-04T09:12:31.512Z"
    }
  ]
}
```

The org balance fields are filled for a key in the organization's default workspace and default
principal. Every first key shown after verified sign-in, and every key made in the console, is
one. A key bound to an explicit workspace or principal gets `null` (see
[Workspaces and principals](/docs/identity)).

| Field | Type | Meaning |
|---|---|---|
| `balance_usd` | number \| null | `credited_usd − spent_usd`, computed from ledger rows on each read. Never a cached counter. |
| `credited_usd` | number \| null | The sum of all credit grants to the org. |
| `spent_usd` | number \| null | The sum of every settled attempt. |
| `recent_attempts[].request_id` | string | The request's `x-request-id`. Pass it to `/v1/generation`. |
| `recent_attempts[].deployment` | string | The deployment that served the attempt. It matches the `x-tm-provider` response header. It is `routerplus` on a closed [dedicated endpoint](/docs/dedicated-endpoints#closed-endpoints) and on any endpoint's dedicated capacity. |
| `recent_attempts[].model` | string | The model id you requested (for an `rp-` name, the Claude model's own id). On a closed dedicated endpoint, the endpoint id. |
| `recent_attempts[].outcome` | string | See [Attempt outcomes](#attempt-outcomes). |
| `recent_attempts[].usage_provenance` | string \| null | `observed`, `estimated` or `unknown`. `null` before settlement. |
| `recent_attempts[].input_tokens` / `output_tokens` | number | Provider-reported counts. Input includes cache tokens. On a model billed at the provider's reported cost, `output_tokens` is the charge in micro-dollars. |
| `recent_attempts[].cost_usd` | number \| null | The settled cost, any web search fee included. `null` until settlement is written (in batches, about every 100 ms). |
| `recent_attempts[].server_tool_use` | object \| null | The web searches and web fetches the provider ran for the model: `web_search_requests`, `web_fetch_requests`. `null` on attempts from before these were recorded. |
| `recent_attempts[].server_tool_fee_usd` | number \| null | The part of `cost_usd` that is web search fees (see [Pricing & billing](/docs/pricing#web-search)). |
| `recent_attempts[].at` | string | The dispatch time. |

Each row is one **attempt**. A request that failed over has one row per dispatch, all with the
same `request_id`.

### Accounting fields

Both endpoints return these fields on each attempt:

| Field | Meaning |
|---|---|
| `billing_source` | `house` for inference paid from RouterPlus credits. `byok` for inference on your own provider connection. |
| `inference_cost_usd` | The cost from the pinned rates and the usage. `null` when either is unknown. |
| `inference_price_source` | `platform_list`, `customer_rate`, `unknown`, or `legacy_platform_list` for old records. |
| `price_snapshot_id` | The id of the pinned rate snapshot. `null` when unknown or not kept for old records. |
| `fee_policy_version` | The pricing policy for this attempt: `house-pass-through-v1` or `byok-free-v1`. |

- `cost_usd` is the RouterPlus charge. Spend caps apply to it.
- `inference_cost_usd` is a separate cost estimate, not a provider invoice. A list-price or
  estimated-usage figure can differ from your provider bill.
- BYOK attempts have a RouterPlus charge of zero and an unknown (`null`) inference cost. See
  [Bring your own key](/docs/byok) and [routing policies](/docs/routing-policies).

## GET /v1/generation?id=

Returns the full per-attempt audit for one request.

| Parameter | In | Required | Meaning |
|---|---|---|---|
| `id` | query | yes | The request id: the `x-request-id` header on every response, or `request_id` in `/v1/usage` rows. |

- An `id` that is not a UUID returns `404 not_found`.
- An unknown id, or one from another org, workspace or principal, returns `404` with
  `"attempts": []` and `"admission_events": []`.

This example is a request that failed over once. The deployment ids are examples. Yours match
your `x-tm-provider` headers.

```bash
curl -s "https://demo-api.routerplus.com/v1/generation?id=0b6f6a4e-6a01-4c5e-9d5a-1c9f2b7e8a33" \
  -H "Authorization: Bearer $TM_API_KEY" | jq
```

```json
{
  "request_id": "0b6f6a4e-6a01-4c5e-9d5a-1c9f2b7e8a33",
  "admission_events": [],
  "attempts": [
    {
      "attempt": 1,
      "deployment": "provider-a",
      "model": "claude-sonnet-4-5",
      "outcome": "failed",
      "usage_provenance": "observed",
      "tokens": { "input": 1200, "cached": 0, "cache_write": 0, "output": 0, "reasoning": 0 },
      "reserved_max_usd": 0.009,
      "cost_usd": 0.0036,
      "billing_source": "house",
      "inference_cost_usd": 0.0036,
      "inference_price_source": "platform_list",
      "price_snapshot_id": "5d2c…",
      "fee_policy_version": "house-pass-through-v1",
      "error_code": "upstream_error",
      "error_origin": "upstream",
      "admission_context": { "…": "…" },
      "route_context": { "…": "…" },
      "dropped_params": [],
      "dispatched_at": "2026-09-04T09:12:31.512Z"
    },
    {
      "attempt": 2,
      "deployment": "provider-b",
      "model": "claude-sonnet-4-5",
      "outcome": "completed",
      "usage_provenance": "observed",
      "tokens": { "input": 1200, "cached": 800, "cache_write": 0, "output": 350, "reasoning": 0 },
      "reserved_max_usd": 0.009,
      "cost_usd": 0.00669,
      "billing_source": "house",
      "inference_cost_usd": 0.00669,
      "inference_price_source": "platform_list",
      "price_snapshot_id": "5d2c…",
      "fee_policy_version": "house-pass-through-v1",
      "error_code": "",
      "error_origin": null,
      "admission_context": { "…": "…" },
      "route_context": { "…": "…" },
      "dropped_params": [],
      "dispatched_at": "2026-09-04T09:12:33.104Z"
    }
  ]
}
```

| Field | Type | Meaning |
|---|---|---|
| `attempt` | number | The dispatch number, starting at 1. |
| `deployment` | string | The deployment that served this attempt. `routerplus` on a closed dedicated endpoint. |
| `outcome` | string | See [Attempt outcomes](#attempt-outcomes). |
| `usage_provenance` | string | `observed` (provider-reported tokens, or a completed attempt), `unknown` (no usage seen), or `estimated`: the provider reported nothing and the gateway priced the attempt itself. On [`/v1/images/generations`](/docs/api-images), a 200 without usage bills `n` × the model's per-image ceiling, the amount the reservation held. On a chat attempt cancelled or cut off before usage arrived, it bills the input (the gateway's estimate, when the provider sent no count) plus the output streamed to you. |
| `tokens` | object | `input` (includes cache tokens), `cached` (reads), `cache_write`, `output`, `reasoning` (part of output). On a model billed at the provider's reported cost, `output` is the charge in micro-dollars. |
| `reserved_max_usd` | number | The worst-case hold taken **before** dispatch (ceiling math). |
| `cost_usd` | number \| null | What settled (floor math), any web search fee included. `null` only before settlement. |
| `server_tool_use` | object \| null | `web_search_requests` and `web_fetch_requests`: the server-tool calls the provider ran for the model. `null` on attempts from before these were recorded. |
| `server_tool_fee_usd` | number \| null | The part of `cost_usd` that is web search fees. |
| `error_code` | string | The error class on failure. Empty on success. |
| `error_origin` | string \| null | Where a failure came from: `upstream`, `gateway_admission`, `upstream_quota`, `gateway_infrastructure` or `authorization`. `null` on success. |
| `admission_context` | object \| null | The limits this attempt claimed, recorded at dispatch. See [Limits and capacity](/docs/admission). |
| `route_context` | object \| null | The route plan behind this attempt: plan id, the deployments planned and excluded, and the policy revisions in force. On a dedicated endpoint, `dedicated` (the endpoint, its revision and the role). |
| `dropped_params` | array | The parameter paths the gateway dropped for this deployment, with the same names as the `x-tm-dropped-params` header. |

`admission_events` lists the limit or balance refusals recorded for this request before any
attempt (`origin`, `scope`, `scope_id`, `limit_kind`, `reason`, `status`, `occurred_at`). It is
empty when nothing was refused.

On a closed [dedicated endpoint](/docs/dedicated-endpoints#closed-endpoints):

- `deployment` and `provider` are `routerplus`, and `model` is the endpoint id.
- `route_context` keeps only `dedicated`, `admission_context` is null and `dropped_params` is
  empty.
- `admission_events` carry no pool `scope_id`.

On any dedicated endpoint, its dedicated capacity reads as `routerplus`.

`reserved_max_usd` is the hold, released in full at settlement. `cost_usd` is the settled
amount, from provider-reported usage, rounded down. See [Pricing & billing](/docs/pricing).

### Attempt outcomes

| `outcome` | Meaning | Billing |
|---|---|---|
| `completed` | The full response was delivered. | Settled on reported usage. |
| `failed` | The attempt failed. A later attempt may have served you. | Settled on any usage the provider reported before it failed (often 0). |
| `cancelled` | You disconnected mid-request. | Settled on the usage the provider reported up to the disconnect. When it reported none: the input plus the output streamed to you before the disconnect, never the hold. If you disconnected before the attempt left the gateway, it settles at zero with provenance `observed`. On `/v1/images/generations` the render is never aborted: it finishes upstream and the attempt settles the provider's reported usage. |
| `incomplete` | The provider failed after your stream started. You received a terminal error event. | Settled on usage observed so far. When there was none: the input plus the output streamed to you. |
| `unknown_after_crash` | The gateway stopped mid-attempt and could not observe the outcome. The attempt is recovered at restart from the gateway's ledger journal, or by the operator reconciler. | Settles **at the reservation**, because the provider may have billed the call. |

> [!WARNING]
> You are debited for **every settled attempt** of a request, including ones
> discarded by failover before your answer started. A provider that fails after reading
> your prompt has still billed prompt tokens. The response's `usage.cost` is the
> request total. In the example above it was `"cost": 0.01029`:
> `0.0036 + 0.00669`, the sum of both attempts.

## Recomputing your bill

The sum of attempt costs equals the response's `usage.cost`:

```bash
curl -s "https://demo-api.routerplus.com/v1/generation?id=$REQ" -H "Authorization: Bearer $TM_API_KEY" \
  | jq '[.attempts[].cost_usd] | add'
```

You can recompute each attempt's cost from its token counts and the published prices. This
script uses the settlement math, including floor division:

```python
import os
import requests
from decimal import Decimal

GATEWAY = "https://demo-api.routerplus.com"
H = {"Authorization": f"Bearer {os.environ['TM_API_KEY']}"}

prices = {m["id"]: m["pricing_usd_per_million"]
          for m in requests.get("https://demo.routerplus.com/api/models.json").json()["models"]}

def per_m(p, sku, fallback):
    # absent SKUs bill at the base rate, never $0
    return int(Decimal(p.get(sku, p[fallback])) * 1_000_000)  # micro-USD per M

def settle_micro(t, p):
    non_cached = max(0, t["input"] - t["cached"] - t["cache_write"])
    reasoning = min(max(0, t["reasoning"]), t["output"])
    plain_out = t["output"] - reasoning
    total = (non_cached        * per_m(p, "prompt", "prompt")
           + t["cached"]       * per_m(p, "cached_prompt", "prompt")
           + t["cache_write"]  * per_m(p, "cache_write", "prompt")
           + plain_out         * per_m(p, "completion", "completion")
           + reasoning         * per_m(p, "internal_reasoning", "completion"))
    return total // 1_000_000  # floor: settlement rounds down

gen = requests.get(f"{GATEWAY}/v1/generation",
                   params={"id": os.environ["REQ"]}, headers=H).json()
micro = sum(settle_micro(a["tokens"], prices[a["model"]])
            for a in gen["attempts"] if a["outcome"] != "unknown_after_crash")
print(micro / 1e6)  # equals the wire's usage.cost
```

- The script leaves out `unknown_after_crash` attempts. They settle at the reservation, not from
  token counts. Compare their `reserved_max_usd`.
- The same math holds for a model billed at the provider's reported cost. Its prices are `"0"`
  in and `"1"` out per million, and `output` is the charge in micro-dollars.
- An attempt with `inference_price_source` `customer_rate` was billed at a
  [dedicated endpoint's](/docs/dedicated-endpoints#prices) contract rate card. Recompute it with
  those prices.
- Attempts settle against **frozen price snapshots** pinned when the request was reserved. A
  recomputation at today's catalog differs only if the price changed since dispatch.

## Who can read what

- Usage and generation reads cover the authenticated workspace and principal, including that
  principal's other keys.
- A request made under another workspace or principal is a 404, like one that never existed.
- Keys bound to an explicit workspace or principal get `null` for the org balance, credit and
  spend totals. A key in the default workspace and principal gets them.

Members of the organization see the org-wide accounting in the console at
[https://demo.routerplus.com/console/usage](https://demo.routerplus.com/console/usage) and
[https://demo.routerplus.com/console/logs](https://demo.routerplus.com/console/logs).

In the console, open Analytics for spend, requests and tokens by model, provider, workspace,
member or key:

![Console Analytics page, grouped by model and then by provider](media/console-analytics.gif)

## Related

- [Pricing & billing](/docs/pricing): the reserve and settle math behind these numbers
- [Errors & remediation](/docs/errors): what each `error_code` means and what to do
- [Quickstart](/docs/quickstart): signup to first billed call
