Troubleshooting
When a request fails, BatchRouter returns a JSON body with a stable, machine-readable code so you can branch on the cause rather than the message:
{ "error": { "code": "insufficient_credits", "message": "Human-readable explanation.", "details": { } }}Always read error.code (and, where present, error.details) — the message text is for humans and may change. This page walks through the failures you are most likely to hit, organized as problem → cause → fix. For the full code reference see Errors, and for limits and backoff see Rate limits.
401 Unauthorized — missing or invalid key
Section titled “401 Unauthorized — missing or invalid key”Problem. Any request returns 401.
Cause. No Authorization header, a malformed header, or a key that was revoked, mistyped, or belongs to a different environment.
Fix.
-
Send the header as
Authorization: Bearer br_live_…. The wordBearer, a single space, then the key. -
Confirm you are calling the right host. A key created in production does not work against
https://test.api.batchrouter.com, and vice versa. -
Verify the key resolves by calling a cheap authenticated endpoint:
Terminal window curl https://api.batchrouter.com/v1/auth/account \-H "Authorization: Bearer $BATCHROUTER_API_KEY"const res = await fetch("https://api.batchrouter.com/v1/auth/account", {headers: { Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}` },});console.log(res.status, await res.json());import os, requestsres = requests.get("https://api.batchrouter.com/v1/auth/account",headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"},)print(res.status_code, res.json()) -
If it still fails, the key was likely revoked. Create a new one with
POST /v1/auth/account/api-keysin the dashboard.
403 Forbidden — authenticated but not allowed
Section titled “403 Forbidden — authenticated but not allowed”Problem. The key is valid (no 401) but the request returns 403.
Cause. The key authenticated, but it is not authorized for that resource — for example, requesting a batch that belongs to another organization, or calling an operator/admin-only path that is not part of the public surface.
Fix. Confirm the batchId belongs to the account that owns the key, and that you are calling a documented public endpoint. List your own batches to verify ownership:
curl https://api.batchrouter.com/v1/batches \ -H "Authorization: Bearer $BATCHROUTER_API_KEY"const res = await fetch("https://api.batchrouter.com/v1/batches", { headers: { Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}` },});console.log(await res.json());import os, requests
res = requests.get( "https://api.batchrouter.com/v1/batches", headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"},)print(res.json())402 Insufficient credits
Section titled “402 Insufficient credits”Problem. POST /v1/batches returns 402 with error.code insufficient_credits.
Cause. Submitting a batch charges credits up front. Your balance does not cover the quoted cost. Quotes (POST /v1/quotes/model) are free and never return 402; the charge happens only at batch creation.
Fix.
-
Buy credits in the dashboard at batchrouter.com/app/billing. There is no public billing-checkout endpoint — purchasing is done in the dashboard.
-
To avoid hitting zero mid-pipeline, enable auto top-up so the platform refills your balance off-session when it drops below a threshold:
Terminal window curl https://api.batchrouter.com/v1/billing/auto-topup \-H "Authorization: Bearer $BATCHROUTER_API_KEY"const res = await fetch("https://api.batchrouter.com/v1/billing/auto-topup", {headers: { Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}` },});console.log(await res.json());import os, requestsres = requests.get("https://api.batchrouter.com/v1/billing/auto-topup",headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"},)print(res.json())Configure it with
PUT /v1/billing/auto-topup, and inspect past refills withGET /v1/billing/auto-topup/attempts. See the API reference for the request fields. -
Check current usage and remaining balance any time with
GET /v1/usage.
400 Preflight validation failed
Section titled “400 Preflight validation failed”Problem. A quote or batch submission returns 400 with error.code batch_preflight_failed.
Cause. Preflight runs before any credits are spent and before the provider quote. It checks the shape and feasibility of your request. The specific issues are listed under error.details.preflight.errors[] — each issue carries a category, a stable code, and an action describing how to fix it:
{ "error": { "code": "batch_preflight_failed", "message": "Batch failed preflight validation.", "details": { "preflight": { "ok": false, "errors": [ { "category": "context_window", "code": "preflight_context_window_exceeded", "message": "Item exceeds the model context window.", "action": "Reduce the input length or choose a model with a larger context window.", "path": "item-42" } ], "warnings": [] } } }}Fix. Read error.details.preflight.errors[] and resolve each by category:
| Category | What it means | Fix |
|---|---|---|
jsonl_shape | An item in your JSONL is malformed or missing required fields. | Validate each line against the canonical item shape. |
file_type | The uploaded file content type is not supported. | Upload JSONL via POST /v1/files using a supported content type (see the API reference). |
context_window | An item’s input is larger than the model’s context window. | Shorten the input, split the item, or pick a model with a larger window via GET /v1/catalog/models. |
tool_support | A requested hosted tool or capability is not available on the routable providers. | Remove the unsupported tool, or constrain routing to a provider that supports it. |
json_schema | A structured-output schema or runtime capability is invalid or unsupported. | Correct the schema, or relax the output constraints. |
webhook | The delivery webhook URL is unsafe or not HTTPS (https_required). | Use a public HTTPS URL; internal, loopback, and metadata hosts are rejected. |
routing | No eligible provider lane can satisfy the request under your routing constraints. | Loosen routing_mode / privacy_tier, or check provider availability with GET /v1/providers. |
409 Conflict — idempotency or terminal state
Section titled “409 Conflict — idempotency or terminal state”Problem. POST /v1/batches returns 409, or a cancel/retry/redeliver call is rejected with 409.
Cause. Either an idempotency conflict — you reused an Idempotency-Key header value with a different request body — or the resource is already in a state that forbids the action (for example cancelling a batch that already reached a terminal status).
Fix.
-
Reusing a key with the same body is fine — a true replay returns the original
202response and the samebat_…id. Only a different payload under the same key conflicts. Generate a fresh, uniqueIdempotency-Key(a UUID works well) for each distinct submission, and reuse the same value only when safely retrying the identical request:Terminal window curl -X POST https://api.batchrouter.com/v1/batches \-H "Authorization: Bearer $BATCHROUTER_API_KEY" \-H "Idempotency-Key: $(uuidgen)" \-H "Content-Type: application/json" \-d '{ "quote_id": "qlock_…" }'import { randomUUID } from "node:crypto";const res = await fetch("https://api.batchrouter.com/v1/batches", {method: "POST",headers: {Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}`,"Idempotency-Key": randomUUID(),"Content-Type": "application/json",},body: JSON.stringify({ quote_id: "qlock_…" }),});console.log(res.status, await res.json());import os, uuid, requestsres = requests.post("https://api.batchrouter.com/v1/batches",headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}","Idempotency-Key": str(uuid.uuid4()),"Content-Type": "application/json",},json={"quote_id": "qlock_…"},)print(res.status_code, res.json()) -
For a state conflict, fetch the batch first (
GET /v1/batches/{batchId}) and check itsstatus. You can only cancel a batch that has not yet reached a terminal status (completed,failed,cancelled,expired), and you can onlyredeliverone whose delivery actually failed.
429 Too Many Requests — rate limited
Section titled “429 Too Many Requests — rate limited”Problem. Requests start returning 429.
Cause. You exceeded the request rate for your account. This is about how often you call the API, not how large a batch is — batching many items into one submission is the intended way to process volume.
Fix.
-
Back off and retry with exponential backoff and jitter. If a
Retry-Afterheader is present, wait at least that long before retrying. -
Coalesce work: submit many items in a single
POST /v1/batchesinstead of many small batches, and poll less aggressively —GET /v1/batches/{batchId}every few seconds is plenty for a long-running batch. -
Prefer webhooks over tight polling loops so you are notified on completion instead of hammering the status endpoint.
See Rate limits for current limits and recommended backoff.
Batch stuck or failed
Section titled “Batch stuck or failed”Problem. A batch is not progressing, or its status is failed.
Cause. Normal progression is pending → queued → routing → dispatched → processing → completing → completed. Time in routing/processing depends on your sla_tier — standard allows up to 24h, while flex may take longer and priority is faster. A failed terminal status means items could not be completed; the details live on the batch and its items.
Fix.
-
Fetch the batch and read its
statusand any failure detail:Terminal window curl https://api.batchrouter.com/v1/batches/bat_123 \-H "Authorization: Bearer $BATCHROUTER_API_KEY"const res = await fetch("https://api.batchrouter.com/v1/batches/bat_123", {headers: { Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}` },});console.log(await res.json());import os, requestsres = requests.get("https://api.batchrouter.com/v1/batches/bat_123",headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"},)print(res.json()) -
Inspect per-item status to find which items failed and why:
Terminal window curl https://api.batchrouter.com/v1/batches/bat_123/items \-H "Authorization: Bearer $BATCHROUTER_API_KEY" -
If the batch failed for a transient reason, re-submit the failed work with
POST /v1/batches/{batchId}/retry. If it is genuinely stuck and no longer needed, cancel it withPOST /v1/batches/{batchId}/cancel. -
For completed batches, pull results with
GET /v1/batches/{batchId}/results(paginated) or fetch a signed file viaGET /v1/batches/{batchId}/artifact-url.
Webhook not received
Section titled “Webhook not received”Problem. Your batch completed but your endpoint never got a delivery callback.
Cause. The most common reasons are a non-HTTPS or unreachable URL, signature verification failing on your side so you reject a delivery that did arrive, or no webhook being configured at all.
Fix.
-
Confirm a webhook is configured — either per batch (
{ "url", "secret" }at submission time) or as an org default viaPUT /v1/auth/account/delivery-webhook. The URL must be HTTPS and publicly reachable; internal and loopback hosts are rejected at preflight. -
Check what BatchRouter actually attempted for the batch, including delivery attempts and responses:
Terminal window curl https://api.batchrouter.com/v1/batches/bat_123/webhooks \-H "Authorization: Bearer $BATCHROUTER_API_KEY" -
Verify the signature correctly. Each call carries
X-BatchRouter-Signature, a base64url HMAC-SHA256 over{timestamp}.{body}using your webhook secret, with the timestamp inX-BatchRouter-Timestamp. If your verification rejects valid deliveries, you will see attempts succeed on our side but never process them. Compare using a constant-time check:import { createHmac, timingSafeEqual } from "node:crypto";function verify(rawBody, timestamp, signature, secret) {const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("base64url");const a = Buffer.from(expected);const b = Buffer.from(signature);return a.length === b.length && timingSafeEqual(a, b);}import hmac, hashlib, base64def verify(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:mac = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256)expected = base64.urlsafe_b64encode(mac.digest()).rstrip(b"=").decode()return hmac.compare_digest(expected, signature)Terminal window # Verification is done in your receiving server, not via cURL.# Recompute base64url( HMAC-SHA256( secret, "{timestamp}.{body}" ) )# and compare it to the X-BatchRouter-Signature header. -
While verifying signatures, sign over the raw request body bytes — re-serializing the JSON can change whitespace and break the HMAC.
-
As a fallback, you do not need webhooks at all: poll
GET /v1/batches/{batchId}until it reachescompleted, then pull results. Webhooks are a convenience, not the only path.
Still stuck?
Section titled “Still stuck?”If you have a reproducible failure, capture the error.code, the full error.details, and the batch id (bat_…) — those identify the exact cause. The interactive API reference lets you replay any request with your key, and the raw spec is at api.batchrouter.com/openapi.json.