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

# Checks and holds

`check()` answers one question before an agent starts work: can this customer pay for it? With `hold: true`, it also reserves the cost, so other work can't spend the same balance while this work runs.

## Describe the work

Pass exactly one of these, and Runledger prices it from the customer's plan:

| Field                       | Use it when                                    | Example                                             |
| --------------------------- | ---------------------------------------------- | --------------------------------------------------- |
| `event_name` and `quantity` | The work is usage you'll track                 | `"agent.task"`, `"1"`                               |
| `outcome_definition`        | The work may produce an outcome you charge for | `"chat_resolution"`                                 |
| `amount`                    | You've already priced it                       | `"40"` (credits, or minor units on a postpaid plan) |

A denial is a normal response with `allowed: false` and a `reason`:

| `reason`                 | Meaning                                                                                                                                    |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `insufficient_balance`   | A prepaid balance can't cover it, after other holds.                                                                                       |
| `credit_limit_reached`   | A postpaid customer's unbilled charges and holds would pass the credit limit.                                                              |
| `no_active_subscription` | The customer isn't on a plan.                                                                                                              |
| `subscription_past_due`  | An invoice is unpaid.                                                                                                                      |
| `no_rate_for_event`      | The plan has no rate for this event or outcome.                                                                                            |
| `allowance_exhausted`    | A plan [usage window](/guides/usage-windows) with `on_exhausted: block` has no room. `denied_by` names it and says when it resets.         |
| `spend_limit_reached`    | A [spend limit](/guides/spend-limits) with `action: block` has no room left in its period. `denied_by` names it and says when it resets.   |
| `margin_below_floor`     | A [margin guardrail](/guides/price-rules-and-guardrails#margin-guardrails) with `action: block` found the work would sell below its floor. |

The response also returns what the work costs (`required`, which is `"0"` for `no_active_subscription` and `no_rate_for_event`, where there is no plan or rate to price it from) and what was `available` before this call, so you can show the customer how far short they are and offer a [top-up](/concepts/billing). The [`affordability`](#how-far-the-work-fits) block gives the same figures for the balance, pools, allowances and spend limits. When an allowance or a spend limit denies the work, `denied_by` names which one and when it lifts.

A denial carries one `reason`. If more than one test would deny the work, which reason you get isn't specified, so handle every reason in the table.

## How far the work fits

Every `check()` response carries an `affordability` block, allowed or denied, with or without `hold: true`. It says what the work would do to the balance, the pools, every allowance that applies and the spend limits, so your product can show a customer "this uses about 40 of your 1,260 credits" before the work starts. The block is always there, and so is every field in it: a value that doesn't apply is `null`, and a list with nothing in it is `[]`. [`POST /v1/events/preview`](/api-reference/events/preview) prices an event and doesn't return it.

**`Response: CheckResult`**

```json title="Response: CheckResult"
{
  "object": "check",
  "allowed": true,
  "customer": "cus_9sF2kq81LmQz",
  "credit_type": "ai_credits",
  "required": "40",
  "available": "1260",
  "affordability": {
    "balance": { "available_before": "1260", "available_after": "1220", "shortfall": "0" },
    "pools": [],
    "allowances": [],
    "spend_limits": [],
    "would_exceed": false,
    "reason": null
  },
  "livemode": false
}
```

A customer with no pools, allowances or limits gets empty lists. When a plan has them, each is listed. Here a spend limit with `action: warn` would be taken 15 past its room: the limit lists its `shortfall` of `"15"`, the work is allowed, and `would_exceed` is `false`, because a warn-only limit doesn't block anything.

**`Response: CheckResult`**

```json title="Response: CheckResult"
{
  "object": "check",
  "allowed": true,
  "customer": "cus_9sF2kq81LmQz",
  "credit_type": "ai_credits",
  "required": "40",
  "available": "1260",
  "affordability": {
    "balance": { "available_before": "1260", "available_after": "1220", "shortfall": "0" },
    "pools": [
      { "pool": "purchased", "available_before": "1260", "would_draw": "40", "available_after": "1220" }
    ],
    "allowances": [],
    "spend_limits": [
      {
        "id": "sl_4kP7wD2nRt",
        "key": "team_monthly_cap",
        "action": "warn",
        "counts": "paid_usage",
        "currency": "usd",
        "counted": "40",
        "remaining_before": "25",
        "remaining_after": "-15",
        "shortfall": "15",
        "resets_at": "2026-12-01T00:00:00Z"
      }
    ],
    "would_exceed": false,
    "reason": null
  },
  "livemode": false
}
```

### Past an allowance: overage, or a stop

Every allowance that applies is listed, and `on_exhausted` says what happens to the part of the work that goes past it. The same numbers read two ways. A plan gives 100 credits a month, 90 are used, and the job needs 30, so the window has `remaining_before` of `"10"`, `remaining_after` of `"-20"` and a `shortfall` of `"20"`.

When the allowance bills overage, the 20 past it are billed as overage. The work is allowed, and `would_exceed` is `false`. This postpaid plan includes 100 credits a month of the `credits` event and bills overage at 5 minor units a credit, so `required` is 20 × 5 = 100:

**`Response: CheckResult`**

```json title="Response: CheckResult"
{
  "object": "check",
  "allowed": true,
  "customer": "cus_9sF2kq81LmQz",
  "currency": "usd",
  "required": "100",
  "available": "50000",
  "affordability": {
    "balance": { "available_before": "50000", "available_after": "49900", "shortfall": "0" },
    "pools": [],
    "allowances": [
      {
        "key": "credits",
        "kind": "included",
        "event_name": "credits",
        "pool": null,
        "on_exhausted": "bill_overage",
        "member_id": null,
        "counted": "30",
        "remaining_before": "10",
        "remaining_after": "-20",
        "shortfall": "20",
        "resets_at": "2026-12-01T00:00:00Z"
      }
    ],
    "spend_limits": [],
    "would_exceed": false,
    "reason": null
  },
  "livemode": false
}
```

When the allowance stops at its cap, the same 20 are over a limit. The work is denied, and `would_exceed` is `true`. This prepaid plan caps use at 100 credits a month, and the work is 30 credits, so the figures are the same. The balance has plenty, so it isn't short:

**`Response: CheckResult`**

```json title="Response: CheckResult"
{
  "object": "check",
  "allowed": false,
  "reason": "allowance_exhausted",
  "customer": "cus_9sF2kq81LmQz",
  "credit_type": "ai_credits",
  "required": "30",
  "available": "1260",
  "denied_by": {
    "type": "allowance",
    "id": "monthly_cap",
    "resets_at": "2026-12-01T00:00:00Z",
    "remaining": "10"
  },
  "affordability": {
    "balance": { "available_before": "1260", "available_after": "1230", "shortfall": "0" },
    "pools": [],
    "allowances": [
      {
        "key": "monthly_cap",
        "kind": "cap",
        "event_name": null,
        "pool": null,
        "on_exhausted": "block",
        "member_id": null,
        "counted": "30",
        "remaining_before": "10",
        "remaining_after": "-20",
        "shortfall": "20",
        "resets_at": "2026-12-01T00:00:00Z"
      }
    ],
    "spend_limits": [],
    "would_exceed": true,
    "reason": "allowance_exhausted"
  },
  "livemode": false
}
```

An allowance with `on_exhausted: draw_next_pool` lists the same kind of shortfall, the amount that moves on to the next pool, and leaves `would_exceed` `false` unless the balance can't cover it.

### What the figures mean

**Units.** Balance and pool amounts are decimal strings in the customer's balance unit: credits of the `credit_type` on a prepaid plan, and minor units of the plan's `currency` on a postpaid plan. Allowance amounts are credits, or units of the event for an included quantity. Spend limit amounts are minor units of that limit's own `currency`. Currencies are never converted or added together.

**Before and after.** A `_before` figure is read before this call's own hold, and counts every other active hold. It is the same figure as `available` on the response. An `_after` figure is the `_before` figure minus what the work uses. Without `hold: true` nothing is reserved, and `_after` is what would be left if the work ran. With `hold: true` on an allowed check the hold is for `required`, so `_after` is what the next caller sees. A denied check places no hold, and its `_after` figures are worked out the same way.

| Field                                     | Type                        | Present                                                                                                                          | How it is worked out                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ----------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `balance.available_before`                | decimal string, or `null`   | `null` for a postpaid plan with no credit limit, and for `no_active_subscription` and `no_rate_for_event`                        | The available balance: posted balance minus active holds on a prepaid plan, credit limit minus unbilled charges minus active holds on a postpaid plan. Negative when overdrawn                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `balance.available_after`                 | decimal string, or `null`   | `null` when `available_before` is `null`                                                                                         | `available_before` minus `required`. Negative when the work doesn't fit                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `balance.shortfall`                       | decimal string, or `null`   | `"0"` when the work fits, and when there is no credit limit to pass. `null` for `no_active_subscription` and `no_rate_for_event` | `required` minus `available_before` when that is above zero                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `pools`                                   | list                        | `[]` on a postpaid plan, and for a prepaid customer whose credits sit in one pool and whose plan has no pools                    | One entry for each pool the customer holds credits in, in drawdown order                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `pools[].pool`                            | string                      | Always                                                                                                                           | The pool key, `default` when the plan has none                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `pools[].available_before`                | decimal string              | Always                                                                                                                           | The pool's posted credits minus active holds on it                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `pools[].would_draw`                      | decimal string              | Always                                                                                                                           | The credits of `required` the pool would cover: the lesser of its `available_before` and what the earlier pools left uncovered. A pool whose cap has `on_exhausted: draw_next_pool` covers no more than the room left in the cap, so it is skipped when there is none. Across all pools it adds up to `required` minus `balance.shortfall`                                                                                                                                                                                                                                                                                                                                 |
| `pools[].available_after`                 | decimal string              | Always                                                                                                                           | `available_before` minus `would_draw`. Never below zero                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `allowances`                              | list                        | `[]` when no window applies                                                                                                      | One entry for every [allowance](/guides/usage-windows) window that applies to the work, not only caps: the plan's `refill` and `cap` allowances, and the included quantity of a postpaid rate. The same windows the [usage meters](/guides/usage-summary) list                                                                                                                                                                                                                                                                                                                                                                                                             |
| `allowances[].key`, `.kind`               | string                      | Always                                                                                                                           | The allowance key (the event name for an included quantity), and `refill`, `cap` or `included`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `allowances[].event_name`                 | string, or `null`           | `null` for a `refill` or `cap`                                                                                                   | The event an `included` quantity counts                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `allowances[].on_exhausted`               | string                      | Always                                                                                                                           | What happens to the part of the work past the window: `block` stops it (a `cap`'s default), `draw_next_pool` moves it to the next pool (a `refill`, or a `cap` set to it), `bill_overage` bills it as overage (an `included` quantity)                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `allowances[].pool`                       | string, or `null`           | `null` when the window isn't tied to a pool                                                                                      | The pool a refill fills or a cap limits. `null` for a cap that counts every pool, and for an included quantity                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `allowances[].member_id`                  | string, or `null`           | `null` for a customer-wide window                                                                                                | The member, for a per-member window                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `allowances[].counted`                    | decimal string              | Always                                                                                                                           | What the work counts in the window: all of `required` for a refill, a cap with no pool and an included quantity (its `quantity`, in units of the event), and the share the pool would cover for a cap with one. Credits, except units for an included quantity                                                                                                                                                                                                                                                                                                                                                                                                             |
| `allowances[].remaining_before`           | decimal string              | Always                                                                                                                           | Credits (units for an included quantity) left in the window, after every other active hold                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `allowances[].remaining_after`            | decimal string              | Always                                                                                                                           | `remaining_before` minus `counted`. Negative when the work goes past the window                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `allowances[].shortfall`                  | decimal string              | Always                                                                                                                           | How far the work is past the window: `counted` minus `remaining_before` when that is above zero, otherwise `"0"`. It is the amount `on_exhausted` applies to: blocked, moved to the next pool, or billed as overage                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `allowances[].resets_at`                  | date-time                   | Always                                                                                                                           | When the window resets                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `spend_limits`                            | list                        | `[]` when no enabled limit applies                                                                                               | One entry for each enabled [spend limit](/guides/spend-limits) that applies to the customer and the work, whether its `action` is `block` or `warn`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `spend_limits[].id`, `.action`, `.counts` | string                      | Always                                                                                                                           | The limit's ID, `warn` or `block`, and `paid_usage` or `all_usage`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `spend_limits[].key`                      | string, or `null`           | `null` when the limit has no key                                                                                                 | The limit's key                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `spend_limits[].currency`                 | string                      | Always                                                                                                                           | The limit's currency. Every amount in the entry is in its minor units                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `spend_limits[].counted`                  | decimal string, minor units | Always                                                                                                                           | The money value of the work that counts toward the limit: the part drawn from paid credits and overage for `paid_usage`, all of it for `all_usage`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `spend_limits[].remaining_before`         | decimal string, minor units | Always                                                                                                                           | The limit's amount minus what is spent and held this period, after every other active hold                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `spend_limits[].remaining_after`          | decimal string, minor units | Always                                                                                                                           | `remaining_before` minus `counted`. Negative when the limit is overrun                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `spend_limits[].shortfall`                | decimal string, minor units | Always                                                                                                                           | `counted` minus `remaining_before` when that is above zero, otherwise `"0"`. A `warn` limit lists it and still allows the work                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `spend_limits[].resets_at`                | date-time                   | Always                                                                                                                           | When the period resets                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `would_exceed`                            | boolean                     | Always                                                                                                                           | `true` only when something would block the work: `balance.shortfall` is above `"0"`, or an `allowances` entry with `on_exhausted: block`, or a `spend_limits` entry with `action: block`, has a `shortfall` above `"0"`. A shortfall that doesn't block doesn't set it: a `warn` spend limit, an allowance that bills overage and an allowance that draws from the next pool all list their `shortfall` and leave it `false`. It is `true` only when the check is denied for lack of room, and `false` on an allowed check, for `no_active_subscription`, `no_rate_for_event` and denials that aren't about room, such as `subscription_past_due` and `margin_below_floor` |
| `reason`                                  | `reason` value, or `null`   | `null` when the check allowed the work                                                                                           | The same as `reason` on the response                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |

A denial still carries `reason` and `denied_by`, and `affordability` shows the figures behind them. For `no_active_subscription` and `no_rate_for_event` there is no balance or price to measure against, so `required` is `"0"`, the `balance` figures are `null`, `pools`, `allowances` and `spend_limits` are `[]`, and `would_exceed` is `false`:

**`Response: CheckResult`**

```json title="Response: CheckResult"
{
  "object": "check",
  "allowed": false,
  "reason": "no_rate_for_event",
  "customer": "cus_9sF2kq81LmQz",
  "credit_type": "ai_credits",
  "required": "0",
  "affordability": {
    "balance": { "available_before": null, "available_after": null, "shortfall": null },
    "pools": [],
    "allowances": [],
    "spend_limits": [],
    "would_exceed": false,
    "reason": "no_rate_for_event"
  },
  "livemode": false
}
```

**Without `hold: true` the block is a point-in-time read, not a reservation.** Another caller can use the balance, an allowance's room or a spend limit's room right after you read it, so a figure that said the work fits can be out of date by the time the work starts. Work that must not lose its place needs `hold: true`. Checks against one balance are [serialized](#two-agents-one-balance), so a hold can't double-spend it. That guarantee is stated for balances, and the block doesn't promise more for caps or spend limits.

## Holds

A hold counts against the available balance until one of these happens:

* **`track()` captures it.** Pass the hold's ID as `hold` on the event. The event's price is captured and the rest released, unless you set `release_remainder: false` to capture again later, for example once per step of a long run.
* **You release it** with `POST /v1/holds/{id}/release`, when the work is canceled or fails.
* **It expires.** The default lifetime is 15 minutes, set by `hold_expires_in` (any ISO 8601 duration, up to 30 days). Expired holds release automatically and send `hold.expired`.

Long-running agents can extend a hold with `POST /v1/holds/{id}/extend`, passing a new `expires_in` and, if the work grew, a new total `amount`. **`expires_in` on an extend is in whole days only**: `P1D` to `P30D`, counted from now, such as `P3D`. A duration in hours or minutes, such as `PT2H`, returns `400` with `parameter_invalid`. That differs from `hold_expires_in` on `check()`, which can be minutes (the default is 15 minutes), so an extend always moves the expiry at least a day out. If you only need a little more time, place the hold with a longer `hold_expires_in` to begin with. A larger total must fit the available balance or the request fails with `insufficient_balance`, and the hold keeps its current amount.

When the captured event is an outcome, the reservation moves to the pending outcome charge and stays held until the charge confirms or is reversed.

## Margin

`check()` can also compare a price with the cost you expect and deny work that would sell below a margin floor. A guardrail's `action` decides what you see: `block` denies the work with the reason `margin_below_floor`, and `warn` allows it and adds the warning `margin_below_guardrail`. `track()` only ever warns, with `margin_below_guardrail`. They are one guardrail seen two ways: `margin_below_floor` is a reason for a denial, and `margin_below_guardrail` is a code on a warning. It takes optional `properties` and `estimated_usage`. See [Pricing from cost](/guides/price-rules-and-guardrails#margin-guardrails).

## Two agents, one balance

This is the failure holds exist to prevent. A customer has 100 credits. Two agents start at the same moment, and each run will cost 60.

|                | Without holds                       | With holds                                   |
| -------------- | ----------------------------------- | -------------------------------------------- |
| Agent A checks | 100 available: allowed              | 100 available: allowed, 60 held              |
| Agent B checks | 100 available: allowed              | 40 available: denied, `insufficient_balance` |
| Both finish    | 120 spent from 100: overdrawn by 20 | 60 spent, 40 left                            |

Checks against one balance are serialized, so two concurrent `check()` calls can never both reserve the same credits. The [sandbox scenarios](/api-reference/test-in-the-sandbox) include this race as a script you can run.

## Holds and changing balances

A hold reserves an amount of a credit type, not particular grants. Credits are drawn from grants only at capture, in the published burn-down order. So a hold placed before the monthly reset survives it, and a hold backed by credits that expire before capture draws from whatever is left. If the balance can no longer cover an active hold, for example after a grant is voided, it shows as overdrawn and the next check is denied.

## Work that already happened

`track()` never fails for lack of balance, because the work is done. If an event costs more than its hold, or there was no hold, the difference is drawn from the available balance. A prepaid balance may go below zero by the plan's `overdraft_limit` (default `"0"`); past it, the balance is overdrawn, `track()` warns `balance_overdrawn`, `balance.depleted` fires, and checks are denied until the customer tops up.

## Postpaid customers

On a postpaid plan, checks and holds work against the credit limit: `available = credit limit - unbilled charges - held`. Without a credit limit, checks are allowed unless the subscription is `past_due`. Holds are still worth placing, because they show what is in flight.

## Limits and windows

A balance says what a customer has. Two more tests say how fast it may be used, and by whom:

* **[Usage windows](/guides/usage-windows)** refill a plan's credits on a schedule and cap use per week, per five-hour session or per hour. A cap that is full denies with `allowance_exhausted`.
* **[Spend limits](/guides/spend-limits)** cap the money value of what a customer, team, member or agent consumes in a period. A limit that is full denies with `spend_limit_reached`.

Active holds count against both, so two agents can't reserve the same room. `track()` never blocks. Work that already happened is recorded, and a window or limit it takes past its amount adds the warning `allowance_exhausted` or `spend_limit_exceeded`. Holds, events and costs record `agent_id`, `member_id` and `team_id`, so you can see and cap spend by any of them. See [Members, teams and agents](/guides/members-teams-agents).

Alerts at thresholds you choose are webhooks: `usage.threshold_reached`, `spend_limit.threshold_reached` and `balance.low`, besides `balance.depleted`. See [Auto top-up and alerts](/guides/auto-top-up-and-alerts).

## Next steps

* [Prepaid and postpaid billing](/concepts/billing) explains the balance a check reads.
* [Going live](/going-live/overview#keep-checks-off-the-critical-path) covers timeouts and failing open.
* [API reference: check](/api-reference/check) lists every field.