BatchRouter Docs

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

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

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 a billing.limit_reached event.
  • 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:

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.

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:

  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.

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.

Next steps

On this page