Skip to content

Spending controls

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.

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

Terminal window
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"
}
}
FieldTypeMeaning
configuredbooleanfalse until you first save controls.
limit_enabledbooleanWhether a spend limit is active.
limit_perioddaily | monthly | nullThe window the limit applies over.
limit_amountMoney | nullSpend cap for the period.
limit_modesoft | hardsoft alerts only; hard blocks new spend at the cap.
limit_timezonestringIANA timezone used to compute period boundaries.
low_balance_amountMoney | nullBalance level that triggers the low-balance alert.
alert_thresholdsarrayThreshold rules (see Alert thresholds).
alert_email_enabledbooleanEmail alerts on/off (default on).
alert_webhook_enabledbooleanAlso deliver alerts to your org delivery webhook (default off).

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.

This example turns on a hard monthly limit of $500 and adds two alert thresholds:

Terminal window
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 — 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 a billing.limit_reached event.
  • hard — blocks new batches once spend would exceed the cap. See How limits affect batch creation.

alert_thresholds is an array (max 20 entries) of { type, value } rules. Each entry uses one of:

typevalue meaningFires when
balance_belowUSD dollarsAvailable balance drops below value.
spend_aboveUSD dollarsPeriod spend rises above value.
pct_of_limitwhole 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.

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:

  1. Quotes never throw on a limit. POST /v1/quotes/model is 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.

  2. Soft limits never block. In soft mode a batch is always accepted; the limit only drives alerts.

  3. 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_reached so the next submit is blocked.

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

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.

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