Spending controls
Set per-org spend limits and alert thresholds with the BatchRouter billing controls API, and understand how hard limits affect batch submission.
Spending controls are guardrails layered on your prepaid credit wallet. You set a per-period spend limit and a set of alert thresholds for your organization, and BatchRouter enforces them at batch-submit time. Controls are configured once per org and apply to every batch your keys submit.
Read and write controls with GET / PUT /v1/billing/controls, and review every change through the audit log at GET /v1/billing/controls/audit. You can also manage all of this from the dashboard at batchrouter.com/app/billing.
Reading controls requires a customer API key or session. Updating them (PUT) requires a signed-in, email-based user session — an API key with no associated user receives 403 user_session_required. Set spend limits from the dashboard or an authenticated browser session, not from a bare br_live_… key.
Read your controls
GET /v1/billing/controls returns the org's current configuration under a controls object. Until you save controls for the first time, configured is false and the fields hold their defaults (limit disabled, soft mode, no thresholds).
curl https://api.batchrouter.com/v1/billing/controls \
-H "Authorization: Bearer $BATCHROUTER_API_KEY"A configured response looks like this:
{
"controls": {
"org_id": "org_123",
"configured": true,
"limit_enabled": true,
"limit_period": "monthly",
"limit_amount": { "currency": "usd", "amount": "500.00" },
"limit_mode": "hard",
"limit_timezone": "America/New_York",
"low_balance_amount": { "currency": "usd", "amount": "25.00" },
"alert_thresholds": [
{ "type": "pct_of_limit", "value": 80 },
{ "type": "balance_below", "value": 25 }
],
"alert_email_enabled": true,
"alert_webhook_enabled": false,
"created_at": "2026-06-01T12:00:00Z",
"updated_at": "2026-06-15T09:30:00Z"
}
}Field reference
| Field | Type | Meaning |
|---|---|---|
configured | boolean | false until you first save controls. |
limit_enabled | boolean | Whether a spend limit is active. |
limit_period | daily | monthly | null | The window the limit applies over. |
limit_amount | Money | null | Spend cap for the period. |
limit_mode | soft | hard | soft alerts only; hard blocks new spend at the cap. |
limit_timezone | string | IANA timezone used to compute period boundaries. |
low_balance_amount | Money | null | Balance level that triggers the low-balance alert. |
alert_thresholds | array | Threshold rules (see Alert thresholds). |
alert_email_enabled | boolean | Email alerts on/off (default on). |
alert_webhook_enabled | boolean | Also deliver alerts to your org delivery webhook (default off). |
On the read model, money fields (limit_amount, low_balance_amount) are returned as a Money object — { "currency": "usd", "amount": "500.00" } — where amount is a decimal string. On the update model they are plain USD-dollar numbers (see below). Refer to the API reference for the exact Money shape.
Update your controls
PUT /v1/billing/controls accepts a partial body — supply only the fields you want to change, and they are merged over the current configuration. Send null to clear a nullable field. Money fields in the request body are plain USD dollar numbers (for example 500 for $500), not Money objects.
If limit_enabled is true, you must supply both limit_period and limit_amount (in the same request or already on record). An enabled limit without a period and amount is rejected with 400.
This example turns on a hard monthly limit of $500 and adds two alert thresholds:
curl -X PUT https://api.batchrouter.com/v1/billing/controls \
-H "Authorization: Bearer $BATCHROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"limit_enabled": true,
"limit_period": "monthly",
"limit_amount": 500,
"limit_mode": "hard",
"limit_timezone": "America/New_York",
"low_balance_amount": 25,
"alert_thresholds": [
{ "type": "pct_of_limit", "value": 80 },
{ "type": "balance_below", "value": 25 }
],
"alert_email_enabled": true
}'The response echoes the full, updated controls object — the same shape as GET.
To remove the limit later, send { "limit_enabled": false }, or clear an amount with { "limit_amount": null }.
Soft vs. hard limits
soft— the safe default for any newly created limit. It never blocks a batch; it only drives alerts so you can watch spend without risking a hard stop. Reaching a soft limit emits abilling.limit_reachedevent.hard— blocks new batches once spend would exceed the cap. See How limits affect batch creation.
Alert thresholds
alert_thresholds is an array (max 20 entries) of { type, value } rules. Each entry uses one of:
type | value meaning | Fires when |
|---|---|---|
balance_below | USD dollars | Available balance drops below value. |
spend_above | USD dollars | Period spend rises above value. |
pct_of_limit | whole percent (0–100) | Period spend reaches that percent of the limit. |
When a threshold fires, BatchRouter emits a billing.* event and, best-effort, sends a customer email (when alert_email_enabled, default on) and a signed delivery webhook (when alert_webhook_enabled, default off). Webhook alerts carry the billing.* event and are HMAC-SHA256 signed with the same X-BatchRouter-Signature scheme as batch delivery webhooks. Alerts are debounced per period so a sweep never spams you.
The dedicated low-balance alert is driven by low_balance_amount and fires once with hysteresis re-arm (it won't re-fire until the balance recovers by roughly 10%). Use alert_thresholds with balance_below only if you want additional balance trip points.
How limits affect batch creation
A spend limit in hard mode is enforced at the authoritative submit gate. When you call POST /v1/batches, BatchRouter computes period spend + in-flight reservations + this batch's estimate. If that projected total would exceed limit_amount for the current period, the batch is rejected at submit with 402 (the same payment-required gate used when credits are insufficient). The error uses the standard error envelope, with limit context in details:
{
"error": {
"code": "insufficient_credits",
"message": "This batch would exceed your monthly spending limit.",
"details": {
"limit": "500.00",
"period": "monthly",
"spent": "480.00",
"projected": "512.00"
}
}
}Key behaviors to design around:
-
Quotes never throw on a limit.
POST /v1/quotes/modelis free and always succeeds for a valid request — it never fails just because the estimate would land near or over your limit. Quote first to check headroom before submitting. -
Soft limits never block. In
softmode a batch is always accepted; the limit only drives alerts. -
Completed batches are never clawed back. Settlement is a backstop — if a finished batch pushes period spend over a hard limit, it still completes, but an overrun emits
billing.limit_reachedso the next submit is blocked. -
Period boundaries use
limit_timezone. Daily and monthly windows are computed from the current time in that timezone, so there is no stored counter that can drift.
A spend limit is separate from your wallet balance. Even with no limit set, a batch that exceeds your available credits is rejected at submit with 402 (insufficient credits). Buy credits in the dashboard — there is no public billing-checkout endpoint. See Credits and billing.
Audit log
Every controls change — yours or a system sweep — writes an entry to the billing-config audit log. Retrieve recent entries, newest first, with GET /v1/billing/controls/audit. The optional limit query parameter (1–200, default 50) caps the page size.
curl "https://api.batchrouter.com/v1/billing/controls/audit?limit=20" \
-H "Authorization: Bearer $BATCHROUTER_API_KEY"Each entry records the actor, the action, and the before/after configuration:
{
"data": [
{
"id": "aud_123",
"org_id": "org_123",
"actor_user_id": "usr_456",
"actor_role": "owner",
"action": "controls.update",
"before": { "limit_enabled": false },
"after": { "limit_enabled": true, "limit_mode": "hard" },
"ip_address": "203.0.113.10",
"created_at": "2026-06-15T09:30:00Z"
}
]
}System or sweep-initiated changes have a null actor_user_id and actor_role. The before and after objects hold the changed configuration; see the API reference for the full entry shape.