---
title: "Spend limits, alerts and automatic top-up"
description: "Keep AI usage spend in check: hard and soft spend limits per customer, subscription or user, limit tiers, alerts with a state per threshold, credit runway, automatic top-up, early usage invoices, a rate-change preview and a usage widget for your app."
canonical_url: "https://twominutebilling.com/help/developers/recipes/spend-limits-and-auto-top-up"
---
# Spend limits, alerts and automatic top-up

> Keep AI usage spend in check: hard and soft spend limits per customer, subscription or user, limit tiers, alerts with a state per threshold, credit runway, automatic top-up, early usage invoices, a rate-change preview and a usage widget for your app.

This recipe continues [Prepaid AI credits](https://twominutebilling.com/help/developers/recipes/prepaid-ai-credits). There,
your app checks the credit before each model call. Here, you put a ceiling on spend, warn the
customer before the ceiling, buy more credit for them before it runs out, and bill large usage
before the period ends.

Every control is off until you create the first one. All amounts are in minor units of the
currency (`50000` = USD 500.00). **Spend** is rated usage before tax and before credit, so a
limit means the same thing whether credit or an invoice pays for the usage.

## Set a spend limit

A limit applies to one customer: all its usage, one subscription, or each value of a usage
property (each user, team or API key). `interval` is `day`, `week` (Monday to Sunday), `month`
(the calendar month in your business time zone) or `period` (the billing period).

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/spend_limits \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "scope": "customer", "interval": "month", "amount_minor": 50000, "currency": "USD", "mode": "hard" }'
```

- `mode: "hard"`: the access check answers `allowed: false` with `code: "spend_limit_reached"`
when the spend in the interval, plus open holds, plus your estimate, is more than the limit.
- `mode: "soft"`: nothing is refused. We send the `spend_limit_reached` event once per interval.

Usage is never refused when you send it. Usage that arrives after the limit is reached is billed.

A refused check names the limit, so your app can tell the user when it resets:

```json
{
  "allowed": false,
  "code": "spend_limit_reached",
  "limits": [
    {
      "id": "slim_XXXXXXXXXXXX01",
      "scope": "customer",
      "interval": "month",
      "amount_minor": 50000,
      "spent_minor": 49980,
      "resets_at": "2026-12-01T00:00:00Z"
    }
  ]
}
```

Shorter intervals cannot be more than longer ones: a weekly limit above the monthly limit is
refused with a 422 that names the field.

### A limit for each user or API key

First add the property to the meter's `limit_dimensions` (at most 3 per meter). Then limit each
value, or one value:

```json
{ "scope": "property", "property_key": "user_id", "interval": "day", "amount_minor": 2000, "currency": "USD", "mode": "hard" }
```

With no `property_value`, each user gets USD 20.00 a day. A row with a value (`"property_value": "user-17"`) wins over it for that user. A property limit counts usage from the time the property
was added to the meter. Send the property in the access check (`scope`) or the limit reports
`status: "not_checked"`.

### Raise limits as customers pay

A **limit tier** raises the customer limit when the customer's lifetime paid amount reaches a
level. The higher of the customer's own limit and the tier applies. A refund never lowers a tier.

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/spend_limit_tiers \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "currency": "USD", "level_minor": 10000, "amounts": { "month": 50000 } }'
```

### Limits your customer sets

In the customer portal, the customer can add a hard limit of its own and lower or delete it. It
can never be more than your limit for the same scope and interval, and the lower of the two
applies. Your limits show in the portal and the customer cannot change them.

## Warn before the limit

An alert watches `spend`, `usage_quantity`, `credit_balance` or `credit_runway`. Thresholds are
absolute or a percent: of the spend limit, of the feature's limit, or of the credit given.

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/usage_alerts \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "spend",
    "customer": "'$CUS'",
    "interval": "month",
    "currency": "USD",
    "thresholds": [{ "value": 50, "unit": "percent" }, { "value": 80, "unit": "percent" }, { "value": 100, "unit": "percent" }],
    "email": true
  }'
```

Send `product` instead of `customer` for a plan alert: it applies to every subscription to the
plan, also to new ones.

Each threshold fires `usage_alert_triggered` once per period. One large jump fires every threshold
that it passes. When the value goes back (a top-up, voided usage) or the period ends, the
threshold sends `usage_alert_resolved`. To rebuild your view without webhooks, read the state of
each threshold:

```bash [curl]
curl "$TMBILLING_URL/v1/businesses/$BIZ/usage_alerts/$ALERT/states?customer=$CUS" \
  -H "Authorization: Bearer $TMBILLING_API_KEY"
```

Each threshold is `armed`, `triggered` or `resolved`, with `triggered_at`, `resolved_at`,
`resolve_reason` and `resets_at`.

## Show the credit runway

```bash [curl]
curl "$TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/credit_runway" \
  -H "Authorization: Bearer $TMBILLING_API_KEY"
```

```json
{
  "object": "credit_runway",
  "currency": "USD",
  "estimate": true,
  "runway_days": 12,
  "runs_out_at": "2026-11-14T09:00:00Z",
  "balance_minor": 6000,
  "burn_minor_per_day": 500,
  "auto_topup_minor": 0,
  "message": "Runs out in about 12 days"
}
```

The burn is the spend of the last 7 days ÷ 7. With less than 24 hours of usage, `runway_days` is
null ("Not enough usage to estimate"). What an automatic top-up can still buy this month is
included.

## Top up credit automatically

One configuration per customer and currency. It buys credit when the balance falls to a
threshold, or on a schedule, and adds the credit only after the payment succeeds:

```bash [curl]
curl -X PUT $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/auto_topup \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "USD",
    "trigger": "threshold",
    "threshold_minor": 1000,
    "method": "fixed",
    "amount_minor": 5000,
    "monthly_cap_minor": 50000,
    "max_runs_per_day": 3,
    "grant": { "tax_treatment": "taxed_at_purchase" }
  }'
```

- `method: "fixed"` adds `amount_minor` (a multiple of it when one is not enough to go above the
threshold). `method: "target"` fills up to `target_minor`.
- Each purchase stays inside the business's top-up limits (`credit_config.top_up_limits`), the
`monthly_cap_minor` and `max_runs_per_day`.
- `"trigger": "schedule"` with `"schedule": { "every": "month", "day": 1 }` or
`{ "every": "week", "weekday": 1 }` buys on fixed dates. Day 31 buys on the last day of a
shorter month.
- With no `payment_method`, we charge the customer's payment methods in their order.

If the payment is declined, we void its invoice, turn automatic top-up off and send
`credit_auto_topup_failed`. We do not try again. Turn it on again with `POST …/auto_topup/enable`
after the customer updates the payment method. `GET …/auto_topup/runs` lists each purchase.

### Mandates in India

On a mandate that needs a notice before each debit, the notice goes out first and the charge
happens after it, 26 to 49 hours later on Indian gateways (10 days for US bank accounts). So a threshold must cover that
time at the current usage. A threshold that covers less is refused at save, with the amount that
is enough. A card or UPI mandate can pay at most ₹15,000 without the payer, so each purchase is at
most ₹15,000. If usage grows past what the threshold covers, we send
`credit_auto_topup_threshold_low` once a week. Until the credit arrives, the access check can
answer `insufficient_credit`.

## Invoice usage early

Bill usage during the period each time the unbilled usage of a subscription reaches an amount:

```bash [curl]
curl -X PUT $TMBILLING_URL/v1/businesses/$BIZ/products/$PLAN/usage_invoice_thresholds/USD \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "amount_minor": 50000 }'
```

`PUT …/subscriptions/$SUB/usage_invoice_threshold` sets an amount for one subscription, or
`{ "amount_minor": null }` turns it off for that subscription. We check about one minute after the
usage arrives. Each early invoice has `purpose: "usage_interim"`, and each usage line shows
`cumulative_minor` (usage to date) and `billed_before_minor`. Discounts and the minimum spend apply
only on the invoice at the end of the period, and all the invoices together bill exactly what one
invoice would. Volume pricing and the `last`, `min` and `avg` aggregations bill only at the end of
the period.

## Preview a rate change

Before you change a rate, see what each customer's last closed period would cost at the new
rate. It writes nothing:

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/prices/$PRICE/rate_change_preview \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "unit_amount_decimal": "0.00025" }'
```

The answer lists `old_minor`, `new_minor`, `diff_minor` and `diff_bps` per customer, largest
increase first, and the totals.

## Show usage in your app

The usage widget shows the balance, the runway, the usage this period, the estimated bill and the
limits, inside your own page. Your backend creates a token for the signed-in customer:

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/usage_widget_tokens \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

The `uwt_…` token reads only that customer's data, accepts no writes and stops working after
15 minutes. Add your app's origin in **Settings → Usage controls** (or `PUT /v1/businesses/$BIZ/settings/usage_widget` with `{ "origins": ["https://app.example.com"] }`),
then copy the snippet from that page:

```html
<script src="https://api.example.com/widget/v1/usage.js" integrity="sha384-…" crossorigin="anonymous"></script>
<div data-usage-widget></div>
<script>
  UsageWidget.mount(document.querySelector("[data-usage-widget]"), {
    getToken: () => fetch("/api/usage-widget-token").then((r) => r.text()),
  });
</script>
```

The widget calls `getToken` again when the token expires. Theme it with CSS variables on the
element: `--uw-font`, `--uw-fg`, `--uw-muted`, `--uw-bg`, `--uw-border`, `--uw-radius` and
`--uw-accent`.

<note>

In the dashboard, open a customer and select **View usage controls** for limits, alerts, runway
and automatic top-up. Plan alerts, limit tiers and the widget are in **Settings → Usage controls**.

</note>


## Sitemap

See the full [sitemap](https://twominutebilling.com/sitemap.md) for all pages.
