Poll & retrieve results
After you submit a batch, BatchRouter routes and executes it asynchronously. This guide shows how to track a batch through its status lifecycle and pull the results back out — either page by page or as a single signed download.
Status lifecycle
Section titled “Status lifecycle”A batch advances through a fixed sequence of statuses. Poll GET /v1/batches/{batchId} and read the top-level status field until it reaches a terminal state.
| Phase | Status | Meaning |
|---|---|---|
| Active | pending | Accepted; not yet enqueued. |
| Active | queued | Waiting for a routing slot. |
| Active | routing | BatchRouter is selecting a provider lane. |
| Active | dispatched | Handed off to the chosen provider. |
| Active | processing | Provider is running your items. |
| Active | completing | Assembling and finalizing results. |
| Terminal | completed | Done — results are available. |
| Terminal | failed | The batch could not complete. |
| Terminal | cancelled | You cancelled the batch. |
| Terminal | expired | The SLA deadline passed before completion. |
Stop polling once status is completed, failed, cancelled, or expired. Results are only available on completed.
Poll batch status
Section titled “Poll batch status”Send your key as a bearer token and read the status field. Pass ?include_billing_receipt=true to embed the finalized billing receipt in the same response once the batch completes.
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}` } });const batch = await res.json();console.log(batch.status, batch.sla_deadline);import os, requests
res = requests.get( "https://api.batchrouter.com/v1/batches/bat_123", headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"},)batch = res.json()print(batch["status"], batch.get("sla_deadline"))A successful response includes the batch id, current status, item_count, created_at, sla_deadline, and the routing_mode / sla_tier you submitted.
Polling with backoff
Section titled “Polling with backoff”Batches are long-running, so poll on an interval — not in a tight loop. Use exponential backoff with a cap, and always stop on a terminal status.
import time, requests, os
TERMINAL = {"completed", "failed", "cancelled", "expired"}
def wait_for_batch(batch_id, max_delay=60): delay = 5 while True: res = requests.get( f"https://api.batchrouter.com/v1/batches/{batch_id}", headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"}, ) batch = res.json() if batch["status"] in TERMINAL: return batch time.sleep(delay) delay = min(delay * 2, max_delay)Retrieve results (paginated)
Section titled “Retrieve results (paginated)”Once status is completed, fetch assembled output from GET /v1/batches/{batchId}/results. Results are paginated: use ?limit (1–1000, default 100) and follow next_cursor.
curl "https://api.batchrouter.com/v1/batches/bat_123/results?limit=100" \ -H "Authorization: Bearer $BATCHROUTER_API_KEY"async function fetchAllResults(batchId) { const out = []; let cursor = null; do { const url = new URL(`https://api.batchrouter.com/v1/batches/${batchId}/results`); url.searchParams.set('limit', '1000'); if (cursor) url.searchParams.set('cursor', cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}` }, }); const page = await res.json(); out.push(...page.results); cursor = page.next_cursor; } while (cursor); return out;}import os, requests
def fetch_all_results(batch_id): out, cursor = [], None while True: params = {"limit": 1000} if cursor: params["cursor"] = cursor res = requests.get( f"https://api.batchrouter.com/v1/batches/{batch_id}/results", headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"}, params=params, ) page = res.json() out.extend(page["results"]) cursor = page["next_cursor"] if cursor is None: break return outThe response wraps a results array and a next_cursor (a string, or null on the last page). Each entry has this shape:
{ "customer_item_id": "item-1", "status": "completed", "output": { "messages": [ { "role": "assistant", "content": "..." } ] }, "error": null}customer_item_id— the id you assigned on submit, for joining back to your input.status—completedorfailedfor that item.output— the model output (object, ornullwhen the item failed).error— failure detail (object, ornullwhen the item succeeded).
Keep requesting pages while next_cursor is non-null. Items can succeed or fail independently, so always check each item’s status rather than assuming a completed batch means every item passed.
Download the full artifact (large batches)
Section titled “Download the full artifact (large batches)”For large batches, paginating thousands of items is slow. Instead, request a short-lived signed URL for the complete results JSONL artifact from GET /v1/batches/{batchId}/artifact-url, then download it directly.
# 1. Get the signed URLcurl https://api.batchrouter.com/v1/batches/bat_123/artifact-url \ -H "Authorization: Bearer $BATCHROUTER_API_KEY"
# 2. Download the artifact (no auth header — the URL is pre-signed)curl -o results.jsonl "<url from step 1>"const meta = await (await fetch( 'https://api.batchrouter.com/v1/batches/bat_123/artifact-url', { headers: { Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}` } })).json();
// meta = { url, expires_at }const jsonl = await (await fetch(meta.url)).text();const items = jsonl.trim().split('\n').map((line) => JSON.parse(line));import os, requests
meta = requests.get( "https://api.batchrouter.com/v1/batches/bat_123/artifact-url", headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"},).json()# meta = { "url": ..., "expires_at": ... }
jsonl = requests.get(meta["url"]).textitems = [__import__("json").loads(line) for line in jsonl.splitlines() if line]The response is { "url": "<signed>", "expires_at": "<timestamp>" }. The signed URL is short-lived — download it promptly and re-request if it expires. The artifact is newline-delimited JSON (JSONL), one result object per line, using the same per-item shape as /results.