> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.runledger.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.runledger.co/_mcp/server.

# Reports and charts

A list shows records, and adding up list pages gives wrong numbers. The reports return totals and time series for a range, so a chart, a finance export and a report to your board agree. This guide shows which report answers which question, and what they have in common.

## Before you start

* Revenue, cost and margin recorded with [`track()` and `trace()`](/guides/cost-attribution-and-margin), for the revenue, margin and cost reports.
* [Credit grants](/concepts/billing#credits) and [usage windows](/guides/usage-windows), for the credits and allowance reports.

## Which report answers what

| Question                                                           | Report                                                                  | Split by                                           |
| ------------------------------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------- |
| Is margin moving, and which customers make or lose money?          | [`GET /v1/runs/summary`](/api-reference/runs/summary)                   | `day`, `week` or `customer`                        |
| Where does revenue come from, and how much is pending or reversed? | [`GET /v1/reports/revenue`](/api-reference/reports/revenue)             | `day`, `week` or `month`, and by invoice line type |
| How many paid credits were sold, used, lapsed and are still owed?  | [`GET /v1/reports/credits`](/api-reference/reports/credits)             | `day`, `week` or `month`                           |
| Are payments failing or being charged back?                        | [`GET /v1/reports/payments`](/api-reference/reports/payments)           | `day`, `week` or `month`, and by provider          |
| Who is close to the end of an allowance?                           | [`GET /v1/reports/allowance_use`](/api-reference/reports/allowance-use) | Ranked by percent used                             |
| What do model calls cost, by model, over time?                     | [`GET /v1/reports/cost`](/api-reference/reports/cost)                   | `day`, `week` or `month`, by provider and model    |

[Margin by customer, model or outcome definition](/api-reference/reports/margin), [outcome counts](/api-reference/reports/outcomes), [breakage](/api-reference/reports/breakage) and [top-up chargebacks](/api-reference/reports/topup-chargebacks) are the older reports. They work the same way, each with its own parameters: margin groups by `customer`, `model` or `outcome_definition` and has no time buckets, so `group_by=day` is a `400`; outcomes buckets by `granularity` and groups by `status`, `outcome_definition`, `reason_code` or `source`; top-up chargebacks group by `provider` or for the whole account.

## What the reports have in common

* **Buckets are UTC.** `group_by` takes `day`, `week` or `month`, and a week starts on Monday. A bucket with nothing in it is returned with zeros, so a chart has no gaps, except in the cost report, which has a row only for a bucket with costs, and the breakage report, which has rows only for credit types with an expiry in the range.
* **One currency.** Amounts are never converted or added across currencies. Send `currency`, or leave it out when everything in the range is in one. If the range has more than one, the call returns `400` with `parameter_invalid` on `currency`.
* **Credits and money are kept apart.** Credits are decimal strings per credit type. Money is in minor units of the report's `currency`. A report never adds one to the other.
* **A run without a margin isn't zero.** A run has a margin only with a confirmed charge and a traced cost. A row with no such run says `has_margin: false` and leaves `margin` and `margin_percent` as `null`, so you can show "no margin data" instead of a flat line.
* **The latest bucket isn't final.** Outcome charges confirm after their window, so revenue for the last few days still moves. Shade the newest bucket in a chart rather than reading a margin from it.
* **Reports read recorded data.** Recorded usage is never repriced, and a new rate card version applies only from its start date. A chart doesn't change when prices do.

## Margin over time and by customer

`GET /v1/runs/summary` totals every run that matches the filters. Add `group_by` and the same totals come back as `rows`:

```bash
curl "https://api.runledger.co/v1/runs/summary?from=2026-10-01T00:00:00Z&to=2026-10-04T00:00:00Z&group_by=day" \
  -H "Authorization: Bearer $RUNLEDGER_API_KEY" \
  -H "Runledger-Version: 2026-10-18"
```

**`Response: RunSummary`**

```json title="Response: RunSummary"
{
  "object": "runs_summary",
  "currency": "usd",
  "runs": 412,
  "runs_with_margin": 371,
  "runs_without_margin": 41,
  "revenue_confirmed": "35240",
  "cost": "9112.5",
  "margin": "26127.5",
  "margin_percent": "0.7414",
  "group_by": "day",
  "rows": [
    {
      "period_start": "2026-10-01T00:00:00Z",
      "period_end": "2026-10-02T00:00:00Z",
      "runs": 148,
      "runs_with_margin": 139,
      "runs_without_margin": 9,
      "revenue_confirmed": "13200",
      "cost": "3380",
      "margin": "9820",
      "margin_percent": "0.7439",
      "has_margin": true
    },
    {
      "period_start": "2026-10-02T00:00:00Z",
      "period_end": "2026-10-03T00:00:00Z",
      "runs": 141,
      "runs_with_margin": 128,
      "runs_without_margin": 13,
      "revenue_confirmed": "12010",
      "cost": "3105.5",
      "margin": "8904.5",
      "margin_percent": "0.7414",
      "has_margin": true
    },
    {
      "period_start": "2026-10-03T00:00:00Z",
      "period_end": "2026-10-04T00:00:00Z",
      "runs": 123,
      "runs_with_margin": 104,
      "runs_without_margin": 19,
      "revenue_confirmed": "10030",
      "cost": "2627",
      "margin": "7403",
      "margin_percent": "0.7381",
      "has_margin": true
    }
  ],
  "livemode": false
}
```

The totals at the top cover every matching run. `group_by=customer` returns one row per customer instead, the highest margin first (confirmed revenue less cost), up to `limit` (50 by default, 500 at most), and sets `has_more` when there are more. A customer whose runs have no margin has `has_margin: false` and sorts last, so a ranking leaves it out. The filters are the ones on [list runs](/api-reference/runs/list).

## Revenue by invoice line type

The [revenue report](/api-reference/reports/revenue) splits `recognized` revenue by the type of invoice line that billed it, in `by_line_type`:

| Type                 | What it bills                                                                                       |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| `usage`              | Usage events, per event name                                                                        |
| `outcome`            | Confirmed outcome charges                                                                           |
| `overage`            | Units past a rate's `included_quantity`                                                             |
| `minimum_commitment` | The shortfall to a commitment. A commitment is a floor, billed as a shortfall line, never a ceiling |

All four are always listed, and they add up to `recognized`. A plan's recurring fee is not in the report: the policy recognizes what usage and outcomes earn, and the fee stays a line on the invoice. Runledger has no separate "committed" or "pay as you go" revenue: what a customer commits to appears as the shortfall line, and everything past the included quantity as `overage`. Add `group_by=month` (or `week` or `day`) to get `periods`, with the same figures for each bucket.

The report applies one set of rules, named by `policy_version`, so two reports with the same version were counted the same way:

* Usage and outcome charges are recognized when they are posted, so a backdated event lands in the period it was recorded and a closed period's figures do not move. A confirmed charge counts at confirmation, and a reversal of a confirmed charge comes off `outcome` and goes into `reversed` in the period it happened. A minimum commitment's shortfall is recognized when the period's invoice finalizes.
* Prepaid credits are valued at the cost basis of the grants they were drawn from. Promotional and included credits bring no revenue. A prepaid charge still pending has drawn nothing, so it is not in `pending`: its credits are still in `deferred`.
* `pending`, `reversible` and `deferred` are standings at the period's end; `reversed` and `breakage` are the period's movements. Buckets are UTC and clipped to the range.

These are operating figures. Confirm the treatment with your adviser before a finance team relies on them.

## Credits sold, used, lapsed and outstanding

The [credits report](/api-reference/reports/credits) covers paid credits, the ones with a cost basis, per credit type. `sold` is what became usable in the bucket, `consumed` is what was drawn less what reversals gave back, `expired` is what lapsed unused, and `outstanding` is what is left at the end of the bucket, valued at the cost basis of its grants in `outstanding_value`. That value is what the [revenue report](/api-reference/reports/revenue) calls deferred. Promotional and included credits that lapse are in the [breakage report](/guides/credit-expiry#report-breakage).

**`Response: CreditsReport`**

```json title="Response: CreditsReport"
{
  "object": "credits_report",
  "currency": "usd",
  "from": "2026-09-01T00:00:00Z",
  "to": "2026-11-01T00:00:00Z",
  "group_by": "month",
  "data": [
    {
      "period_start": "2026-09-01T00:00:00Z",
      "period_end": "2026-10-01T00:00:00Z",
      "credit_type": "ai_credits",
      "sold": "480000",
      "consumed": "395000",
      "expired": "8000",
      "outstanding": "1740000",
      "outstanding_value": { "amount": 1392000, "currency": "usd" }
    },
    {
      "period_start": "2026-10-01T00:00:00Z",
      "period_end": "2026-11-01T00:00:00Z",
      "credit_type": "ai_credits",
      "sold": "500000",
      "consumed": "410000",
      "expired": "12000",
      "outstanding": "1818000",
      "outstanding_value": { "amount": 1454400, "currency": "usd" }
    }
  ],
  "generated_at": "2026-11-01T00:05:00Z",
  "livemode": false
}
```

## Payments and failures

The [payments report](/api-reference/reports/payments) counts payments by status over time: `succeeded`, `failed` and `disputed`, each with a `count` and an `amount`. A payment is counted once, in the bucket it was created in, under its current status, and a payment still `pending` isn't in the report. Send `split_by=provider` for a row per provider in each bucket, and `kind` to look at top-ups or invoice payments alone.

**`Response: PaymentsReport`**

```json title="Response: PaymentsReport"
{
  "object": "payments_report",
  "currency": "usd",
  "from": "2026-10-19T00:00:00Z",
  "to": "2026-10-26T00:00:00Z",
  "group_by": "week",
  "split_by": "provider",
  "data": [
    {
      "period_start": "2026-10-19T00:00:00Z",
      "period_end": "2026-10-26T00:00:00Z",
      "provider": "stripe",
      "succeeded": { "count": 1240, "amount": 9840000 },
      "failed": { "count": 31, "amount": 212000 },
      "disputed": { "count": 3, "amount": 41000 }
    },
    {
      "period_start": "2026-10-19T00:00:00Z",
      "period_end": "2026-10-26T00:00:00Z",
      "provider": "adyen",
      "succeeded": { "count": 410, "amount": 3120000 },
      "failed": { "count": 12, "amount": 86000 },
      "disputed": { "count": 0, "amount": 0 }
    }
  ],
  "generated_at": "2026-10-26T00:05:00Z",
  "livemode": false
}
```

A card chargeback on a top-up is a payment in `disputed`. It isn't an [outcome dispute](/concepts/outcome-settlement#disputes). [Top-up chargebacks](/concepts/payment-providers#chargebacks-on-top-ups) has the rate and the credits at risk.

## Who is close to the end of an allowance

The [allowance use report](/api-reference/reports/allowance-use) lists each customer's allowance, in the window the customer is in now, ranked by `percent_used`. Send `percent_used_over` to keep only customers past a share, such as `0.8` for more than 80%. It has no money in it. For one customer's windows, with holds and the one that binds first, use the [usage summary](/guides/usage-summary).

```bash
curl "https://api.runledger.co/v1/reports/allowance_use?percent_used_over=0.8&allowance=weekly" \
  -H "Authorization: Bearer $RUNLEDGER_API_KEY" \
  -H "Runledger-Version: 2026-10-18"
```

**`Response: AllowanceUseReport`**

```json title="Response: AllowanceUseReport"
{
  "object": "allowance_use_report",
  "as_of": "2026-10-26T09:00:00Z",
  "data": [
    {
      "customer": "cus_9sF2kq81LmQz",
      "key": "weekly",
      "kind": "refill",
      "credit_type": "ai_credits",
      "pool": "included",
      "period_start": "2026-10-26T00:00:00Z",
      "resets_at": "2026-11-02T00:00:00Z",
      "granted": "1500",
      "used": "1440",
      "remaining": "60",
      "percent_used": "0.96"
    },
    {
      "customer": "cus_4dN7pQ20XaRt",
      "key": "weekly",
      "kind": "refill",
      "credit_type": "ai_credits",
      "pool": "included",
      "period_start": "2026-10-26T00:00:00Z",
      "resets_at": "2026-11-02T00:00:00Z",
      "granted": "1500",
      "used": "1320",
      "remaining": "180",
      "percent_used": "0.88"
    }
  ],
  "has_more": false,
  "generated_at": "2026-10-26T09:00:01Z",
  "livemode": false
}
```

For an allowance with `scope: per_member`, a row is the member who has used the most, named in `member_id`. The report returns up to `limit` rows (50 by default, 500 at most) and sets `has_more` when there are more.

## Cost by model over time

The [cost report](/api-reference/reports/cost) is cost alone: one row per bucket, provider and model, from every cost recorded with `trace()` or `POST /v1/costs`, whether or not a run has revenue. Use it to see which model is driving spend, and the [margin report](/api-reference/reports/margin) when you want cost beside revenue. A provider price change shows up in it as a step from the date the new price applies.

**`Response: CostReport`**

```json title="Response: CostReport"
{
  "object": "cost_report",
  "currency": "usd",
  "from": "2026-10-19T00:00:00Z",
  "to": "2026-10-26T00:00:00Z",
  "group_by": "week",
  "data": [
    {
      "period_start": "2026-10-19T00:00:00Z",
      "period_end": "2026-10-26T00:00:00Z",
      "provider": "anthropic",
      "model": "claude-sonnet-4",
      "cost": "18420.5"
    },
    {
      "period_start": "2026-10-19T00:00:00Z",
      "period_end": "2026-10-26T00:00:00Z",
      "provider": "openai",
      "model": "gpt-4.1-mini",
      "cost": "6310"
    }
  ],
  "generated_at": "2026-10-26T00:05:00Z",
  "livemode": false
}
```

## Export the rows

`POST /v1/exports` writes a file of the API's own rows for your warehouse or your finance team: `balances` as of a moment (`as_of`), `ledger_entries` or `outcome_charges` created in a period, or the `revenue` report by day, as `csv` or `jsonl`. The export starts `pending` while the worker writes the file, and `export.ready` or `export.failed` says when it is done. `GET /v1/exports/{export_id}` then signs a `download_url` that lasts an hour; read it again for a fresh one, since `GET /v1/exports` lists exports without their links. The file is kept for seven days, after which the export is `expired`.

## API reference

[Run summary](/api-reference/runs/summary), [revenue report](/api-reference/reports/revenue), [credits report](/api-reference/reports/credits), [payments report](/api-reference/reports/payments), [allowance use report](/api-reference/reports/allowance-use) and [cost report](/api-reference/reports/cost).

## Next steps

* [Cost attribution and margin](/guides/cost-attribution-and-margin) explains how a run gets its revenue, cost and margin.
* [Credit expiry and breakage](/guides/credit-expiry) covers the credits that lapse.
* [Usage meters](/guides/usage-summary) show one customer's windows.