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

# Spend limits

A spend limit caps the money value of the work a scope consumes in a period. It answers "how much is this team allowed to spend this month?", which a credit balance can't, because a balance says what a customer has and not what they are willing to use.

## Before you start

* You call [`check()` and `track()`](/concepts/checks-and-holds) for each piece of work.
* For limits by member, team or agent, you send [`member_id`, `team_id` or `agent_id`](/guides/members-teams-agents) on those calls.

With no spend limit there is no cap. Limits sit beside the balance. `check()` tests the balance and every limit that applies.

## What a limit counts

A limit counts the money value of consumption, not credits. That keeps one limit comparable across plans, pools and burn multipliers.

| `counts`               | What counts toward the limit                                                                                                                                                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paid_usage` (default) | Credits drawn from paid grants, valued at each grant's `cost_basis`, and on a postpaid plan the confirmed and pending charges. Included and promotional credits count as zero, so a customer spending free credits isn't stopped by a money limit |
| `all_usage`            | Everything, with credits that have no cost basis valued at the plan's `top_up.unit_amount_decimal`. A plan with no top-up price values them at zero                                                                                               |

Buying credits never counts. Only consumption does. Active [holds](/concepts/checks-and-holds) count as spent until they are captured or released, so two agents can't both reserve the last of a budget.

## Where a limit applies

`applies_to` holds the conditions a limit needs. Every condition you send must match.

| Condition   | Limits                                     |
| ----------- | ------------------------------------------ |
| `customer`  | One customer                               |
| `plan`      | Every customer on a plan                   |
| `agent_id`  | Work done by one of your agents            |
| `member_id` | One person inside a customer               |
| `team_id`   | One team inside a customer                 |
| `pool`      | Consumption from one [pool](/guides/pools) |
| none        | The whole account                          |

`agent_id`, `member_id` and `team_id` are the values you send on `check()`, `track()` and `trace()`. Runledger doesn't model members or teams; they are labels you choose. See [Members, teams and agents](/guides/members-teams-agents).

## Which limit wins

Limits are grouped by which of `agent_id`, `member_id`, `team_id` and `pool` they name. A limit that names none of them is in the customer group.

1. **Inside a group, the most specific limit applies.** For a customer that is a limit on the customer, then one on the customer's plan, then one on the account.
2. **Across groups, the strictest wins.** The work must have room in every group that applies. If the customer group has $500 left and the team group has $20, the work is denied once it would pass \$20.
3. **A customer can only tighten.** A limit your customer sets (`set_by: customer`) replaces the operator's limit in its group, and it can't be higher than that limit's `ceiling`, or than its `amount` when it has no ceiling. A higher value returns `parameter_invalid`.

## Create a limit

**`POST /v1/spend_limits`**

```json title="POST /v1/spend_limits"
{
  "key": "growth_team_monthly",
  "applies_to": { "customer": "cus_9sF2kq81LmQz", "team_id": "team_growth" },
  "period": { "duration": "P1M", "anchor": "billing_period" },
  "amount": { "amount": 200000, "currency": "usd" },
  "action": "block",
  "alert_at": ["0.75", "0.9", "1"]
}
```

**`Response: SpendLimit`**

```json title="Response: SpendLimit"
{
  "id": "sl_4kP7wD2nRt",
  "object": "spend_limit",
  "key": "growth_team_monthly",
  "applies_to": { "customer": "cus_9sF2kq81LmQz", "team_id": "team_growth" },
  "period": { "duration": "P1M", "anchor": "billing_period" },
  "amount": { "amount": 200000, "currency": "usd" },
  "counts": "paid_usage",
  "action": "block",
  "alert_at": ["0.75", "0.9", "1"],
  "set_by": "operator",
  "enabled": true,
  "current": {
    "spent": { "amount": 0, "currency": "usd" },
    "held": { "amount": 0, "currency": "usd" },
    "period_start": "2026-11-01T00:00:00Z",
    "resets_at": "2026-12-01T00:00:00Z"
  },
  "actor": { "type": "key", "id": "key_1xB6nW3dQz", "name": "Billing backend" },
  "livemode": false,
  "created_at": "2026-11-04T13:12:09Z"
}
```

* **Period.** `P1D`, `P1W` or `P1M`. `anchor` is `calendar` (UTC midnight, Monday, the first of the month), `billing_period` (the customer's subscription period) or `customer` (the moment the subscription was created, today the same as `subscription_start`).
* **Action.** `warn` adds the warning `spend_limit_exceeded` and sends a webhook, and `check()` still allows the work. `block` also makes `check()` deny the work. The default is `warn`.
* **Amount.** Its currency must be the customer's currency, or the plan's.
* **Changes.** `PATCH /v1/spend_limits/{id}` changes `amount`, `ceiling`, `counts`, `action`, `alert_at` and `enabled`, and a field you leave out keeps its value. `applies_to` and `period` can't change; delete the limit and create a new one. The limit's `current` shows what has been spent this period.

`GET /v1/spend_limits` lists limits, newest first, and filters by `customer`, `plan`, `agent_id`, `member_id`, `team_id`, `set_by` and `enabled`.

## What a denial looks like

A `block` limit denies in `check()` with the reason `spend_limit_reached`. `denied_by` names the limit and when it lifts, so your product can tell the person what to do.

**`Response: CheckResult`**

```json title="Response: CheckResult"
{
  "object": "check",
  "allowed": false,
  "reason": "spend_limit_reached",
  "customer": "cus_9sF2kq81LmQz",
  "credit_type": "ai_credits",
  "required": "40",
  "available": "1260",
  "denied_by": {
    "type": "spend_limit",
    "id": "sl_4kP7wD2nRt",
    "resets_at": "2026-12-01T00:00:00Z",
    "member_id": "user_2231",
    "remaining": "0"
  },
  "affordability": {
    "balance": { "available_before": "1260", "available_after": "1220", "shortfall": "0" },
    "pools": [],
    "allowances": [],
    "spend_limits": [
      {
        "id": "sl_4kP7wD2nRt",
        "key": null,
        "action": "block",
        "counts": "paid_usage",
        "currency": "usd",
        "counted": "40",
        "remaining_before": "0",
        "remaining_after": "-40",
        "shortfall": "40",
        "resets_at": "2026-12-01T00:00:00Z"
      }
    ],
    "would_exceed": true,
    "reason": "spend_limit_reached"
  },
  "livemode": false
}
```

A spend limit with `action: warn` is listed in `affordability.spend_limits` the same way, with its `remaining_before`, `remaining_after` and `shortfall`, but it doesn't set `would_exceed`, because the work is allowed. `would_exceed` is `true` only when something would block: a balance shortfall, an allowance that stops at its cap, or a spend limit with `action: block`. See [Checks and holds](/concepts/checks-and-holds#how-far-the-work-fits).

`track()` never blocks, because the work already happened. If it takes a limit past its amount, the event is recorded and carries the warning `spend_limit_exceeded`.

## Alerts

`alert_at` lists decimals from 0 to 1. The first time spend reaches each one in a period, Runledger sends `spend_limit.threshold_reached`, once per period per threshold. A threshold of `"1"` fires when the limit is full. The event's `context` says which limit, which `member_id` and `team_id`, and the threshold. See [Auto top-up and alerts](/guides/auto-top-up-and-alerts).

## Who can change what

| Limit                                                      | Created by                                                                       | In live mode                                                                        |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Account, plan, agent, member, team or pool                 | You, as [configuration](/api-reference/how-configuration-works)                  | A config proposal an Admin approves. A live key gets `403` with `proposal_required` |
| One customer, including when a team or member is named too | Your backend, or the customer through the [customer view](/guides/customer-view) | Allowed with a key that has `spend_limits:write`                                    |
| A plan's default (`default_spend_limit`)                   | You, on the plan                                                                 | Part of the plan, so it follows plan rules                                          |

A limit for one customer is data, like a credit grant. It isn't part of your configuration, and a customer changing their own limit is never held for approval.

In live mode an agent key can create a customer limit and tighten one, and can't loosen or delete one: a higher `amount` or `ceiling`, `counts` narrowed from `all_usage` to `paid_usage`, `action` from `block` to `warn`, an `alert_at` threshold raised or dropped, or `enabled: false` is refused, and so is a customer-set limit it creates that is looser in any of those than the limit it replaces. That refusal is `permission_denied`, and it applies only to agent keys. This keeps an AI agent that is working for your customer from loosening the guard rails around itself.

Operator limits belong in the [configuration document](/api-reference/how-configuration-works) under `spend_limits`, matched by `key`, so they go through dry run, the sandbox and a live proposal like everything else.

## Plan defaults

A plan's `default_spend_limit` gives each new subscription a limit that your customer can replace with their own, up to `ceiling`.

**`Config document: plans`**

```json title="Config document: plans"
{
  "plans": [
    {
      "key": "pro_monthly",
      "name": "Pro",
      "billing_mode": "prepaid",
      "currency": "usd",
      "interval": "month",
      "credit_type": "ai_credits",
      "recurring_fee": { "amount": 4900, "currency": "usd" },
      "included_credits": { "amount": "5000" },
      "top_up": { "unit_amount_decimal": "1", "minimum_credits": "1000", "maximum_credits": "1000000" },
      "default_spend_limit": {
        "amount": { "amount": 50000, "currency": "usd" },
        "ceiling": { "amount": 500000, "currency": "usd" },
        "action": "block",
        "alert_at": ["0.75", "0.9", "1"]
      },
      "rates": [{ "event_name": "agent.task", "credits_per_unit": "10" }]
    }
  ]
}
```

`default_spend_limit` can change at any time and applies to subscriptions created afterwards. Existing subscriptions keep the limit they have.

## Record keeping

Every create, change and delete is an audit event with the actor, and every limit shows who last changed it. A customer who sets a limit on the customer view is recorded as actor `system` named `customer_session`, with the session's ID. Spend counters are derived from the [ledger](/concepts/billing#the-ledger), so they never drift from it.

## Sandbox

Create limits with a sandbox key and use a [test clock](/api-reference/test-in-the-sandbox) to cross a period boundary. The `spend_limit` scenario in the MCP server's `run_scenario` tool spends against a limit until a `block` denies the work.

## API reference

[Create](/api-reference/spend-limits/create), [list](/api-reference/spend-limits/list), [retrieve](/api-reference/spend-limits/retrieve), [update](/api-reference/spend-limits/update) and [delete](/api-reference/spend-limits/delete) a spend limit.

## Next steps

* [Members, teams and agents](/guides/members-teams-agents) shows how usage is attributed.
* [Auto top-up and alerts](/guides/auto-top-up-and-alerts) covers the alerts a limit can send.
* [Customer view](/guides/customer-view) lets customers set their own limits.
* [Going live](/going-live/overview) lists what to check before real billing starts.