Spend limits, alerts and automatic top-up
This recipe continues 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).
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 answersallowed: falsewithcode: "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 thespend_limit_reachedevent 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:
{
"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:
{ "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.
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.
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:
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
curl "$TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/credit_runway" \
-H "Authorization: Bearer $TMBILLING_API_KEY"
{
"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:
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"addsamount_minor(a multiple of it when one is not enough to go above the threshold).method: "target"fills up totarget_minor.- Each purchase stays inside the business's top-up limits (
credit_config.top_up_limits), themonthly_cap_minorandmax_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:
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:
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:
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:
<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.
Prepaid AI credits
Sell prepaid credit for AI usage: a credit unit, credit packs and plan credits, an access check before each call, and holds that stop parallel calls from spending the same credit.
Authentication
Authenticate with a bearer API key per organization, scoped to test mode or live mode, and learn what the mode check refuses and why that protects your data.