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

# Credit expiry and breakage

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 delivered as [credit grants](/concepts/billing#credits), in [pools](/guides/pools).

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 from                    | Expires                                                                                                                                                       |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A plan's refill allowance       | At the end of the window, unless `rollover` keeps some. See [Usage windows](/guides/usage-windows)                                                            |
| Rollover                        | At the end of the next window, or after `rollover_expires_after`                                                                                              |
| A top-up                        | After 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 view | The same as a top-up: the plan's `top_up.credits_expire_in`, or one year                                                                                      |
| An auto top-up                  | The same as a top-up: the plan's `top_up.credits_expire_in`, or one year                                                                                      |
| `POST /v1/credit_grants`        | At 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`**

```json title="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`**

```json title="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](/concepts/billing#credits) 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](/guides/usage-windows) refill doesn't send it: its credits lapse when its window resets. Set it in the configuration document:

**`Config document: account_settings`**

```json title="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](/guides/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:

| Class         | What it is                                                                  |
| ------------- | --------------------------------------------------------------------------- |
| `included`    | Credits from allowances and rollovers. No cash was paid for them            |
| `promotional` | Free credits you granted                                                    |
| `paid`        | Credits a customer paid for. `paid_value` values them at their `cost_basis` |

**`Response: BreakageReport`**

```json title="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](/api-reference/reports/credits).

Credits that were paid for count as deferred revenue until they are used or lapse. The revenue [exports](/concepts/billing#the-ledger) 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](/concepts/checks-and-holds#holds-and-changing-balances).
* A [test clock](/api-reference/test-in-the-sandbox) moves expiry, so you can test the warning and the lapse without waiting.

## API reference

[Breakage report](/api-reference/reports/breakage), and [create a credit grant](/api-reference/credit-grants/create).

## Next steps

* [Auto top-up and alerts](/guides/auto-top-up-and-alerts) buys credits before a balance runs out.
* [Usage meters](/guides/usage-summary) show customers the next expiry.
* [Sandbox and test clocks](/api-reference/test-in-the-sandbox) shows how to test an expiry without waiting.