Skip to navigation

Reports and charts

Read totals, trends and rankings from the API, the same figures the dashboard charts use.

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

Which report answers what

QuestionReportSplit by
Is margin moving, and which customers make or lose money?GET /v1/runs/summaryday, week or customer
Where does revenue come from, and how much is pending or reversed?GET /v1/reports/revenueday, week or month, and by invoice line type
How many paid credits were sold, used, lapsed and are still owed?GET /v1/reports/creditsday, week or month
Are payments failing or being charged back?GET /v1/reports/paymentsday, week or month, and by provider
Who is close to the end of an allowance?GET /v1/reports/allowance_useRanked by percent used
What do model calls cost, by model, over time?GET /v1/reports/costday, week or month, by provider and model

Margin by customer, model or outcome definition, outcome counts, breakage and top-up 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:

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
{
"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.

Revenue by invoice line type

The revenue report splits recognized revenue by the type of invoice line that billed it, in by_line_type:

TypeWhat it bills
usageUsage events, per event name
outcomeConfirmed outcome charges
overageUnits past a rate’s included_quantity
minimum_commitmentThe 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 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 calls deferred. Promotional and included credits that lapse are in the breakage report.

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 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
{
"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. Top-up chargebacks has the rate and the credits at risk.

Who is close to the end of an allowance

The allowance use report 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.

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
{
"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 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 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
{
"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, revenue report, credits report, payments report, allowance use report and cost report.

Next steps