Skip to navigation

Credit expiry and breakage

Let credits expire, warn customers first, and report what lapsed.

Every credit grant can have an expires_at. Unused credits at that time are removed with a grant_expired ledger entry and a credit_grant.expired event. This guide covers where expiry comes from, how customers are warned and how the lapsed credits are reported.

Before you start

Credits bought in a top-up expire after one year unless you set a different length. Setting an expiry on paid credits that is lawful where your customers are is your responsibility, whether you keep the one-year default or change it, and Runledger doesn’t check one against any law. Nothing here is legal advice, and legal review of credit expiry is part of your pre-launch checklist.

Where an expiry comes from

Credits fromExpires
A plan’s refill allowanceAt the end of the window, unless rollover keeps some. See Usage windows
RolloverAt the end of the next window, or after rollover_expires_after
A top-upAfter one year, unless the plan’s top_up.credits_expire_in or the checkout session’s credits_expire_in says otherwise. The session’s wins over the plan’s
A top-up from the customer viewThe same as a top-up: the plan’s top_up.credits_expire_in, or one year
An auto top-upThe same as a top-up: the plan’s top_up.credits_expire_in, or one year
POST /v1/credit_grantsAt the expires_at you send

Purchased credits expire after one year by default: leave credits_expire_in out and a top-up’s credits last for one year (P1Y) from when payment completes. Set credits_expire_in on the plan’s top_up, or on one checkout session, to use a different length. The default is the same through the API, the customer view and the dashboard. Grants you create yourself with POST /v1/credit_grants have only the expires_at you send.

Config document: plans
{
"plans": [
{
"key": "payg",
"name": "Pay as you go",
"billing_mode": "prepaid",
"currency": "usd",
"interval": "month",
"credit_type": "ai_credits",
"top_up": {
"unit_amount_decimal": "1",
"minimum_credits": "1000",
"maximum_credits": "1000000",
"credits_expire_in": "P1Y"
},
"rates": [{ "event_name": "agent.task", "credits_per_unit": "10" }]
}
]
}

A checkout session can override the plan’s setting for one purchase:

POST /v1/checkout_sessions
{
"external_customer_id": "org_123",
"mode": "top_up",
"credits": "5000",
"credits_expire_in": "P6M",
"success_url": "https://app.example.com/billing/done",
"cancel_url": "https://app.example.com/billing"
}

The duration runs from when payment completes, and goes from P1D to P10Y. The grant’s expires_at is set then, one year on when no duration is given.

Spent first

The burn order uses soonest expiry as its second rule, so credits closest to lapsing are spent first inside a priority. Customers lose the least when the oldest credits go before the newest. Promotional credits before paid is the third rule, so free credits are used before ones the customer paid for.

Warn customers

credit_grant.expiring is sent once before a grant with credits left expires. The lead time is the account setting grant_expiry_notice, an ISO 8601 duration, P7D by default, counted on the customer’s clock, so a test clock sends it when it is advanced past that point. A grant that expires sooner than the notice is told at once. P0D turns the notice off, and a change to the setting moves the notice of every grant that hasn’t expired. An allowance’s refill doesn’t send it: its credits lapse when its window resets. Set it in the configuration document:

Config document: account_settings
{
"account_settings": { "grant_expiry_notice": "P14D" }
}

The event names the grant, and its context carries the customer. Fetch the grant for the amount remaining and tell the customer in your own product, or let them see the credits that expire next in the customer view. The usage summary lists the next expiry for each pool.

Report breakage

GET /v1/reports/breakage shows credits that lapsed in a range, grouped by day, week or month, in three classes:

ClassWhat it is
includedCredits from allowances and rollovers. No cash was paid for them
promotionalFree credits you granted
paidCredits a customer paid for. paid_value values them at their cost_basis
Response: BreakageReport
{
"object": "breakage_report",
"from": "2026-10-01T00:00:00Z",
"to": "2026-11-01T00:00:00Z",
"group_by": "month",
"rows": [
{
"period_start": "2026-10-01T00:00:00Z",
"period_end": "2026-11-01T00:00:00Z",
"credit_type": "ai_credits",
"included": "120000",
"promotional": "5000",
"paid": "40000",
"paid_value": { "amount": 40000, "currency": "usd" }
}
],
"livemode": false
}

For paid credits sold, used, lapsed and still outstanding over time, use the credits report.

Credits that were paid for count as deferred revenue until they are used or lapse. The revenue exports release lapsed paid credits at their cost basis in the period they expire, so the report and the export agree. Lapsed included and promotional credits never had revenue and release nothing.

Rules

  • A voided grant isn’t expiry. Voiding keeps its reason and doesn’t appear in the breakage report.
  • Expiry is checked against the ledger at the second. A hold that depends on credits that lapse before capture draws from what is left, as in Checks and holds.
  • A test clock moves expiry, so you can test the warning and the lapse without waiting.

API reference

Breakage report, and create a credit grant.

Next steps