Batch status lifecycle
Every batch you create moves through a fixed sequence of statuses, from acceptance to a single terminal outcome. You observe this progression by polling GET /v1/batches/{batchId} and reading the status field. This page is the reference for what each status means, which ones are terminal, and what your client should do at each step.
The status enum is also accepted as a filter on GET /v1/batches?status=..., so the same values name both a batch’s current state and a list filter.
The state machine
Section titled “The state machine”A batch advances through up to seven non-terminal statuses in order, then settles into exactly one of four terminal statuses. It never moves backward.
pending → queued → routing → dispatched → processing → completing → completed ‖ failed · cancelled · expiredYou will not necessarily observe every intermediate status — a fast batch can pass through several between two polls. Treat the sequence as the guaranteed order, not as a guarantee that you will see each one.
Status reference
Section titled “Status reference”| Status | Terminal? | Meaning | What to do |
|---|---|---|---|
pending | No | The batch was accepted (POST /v1/batches returned 202) and is being prepared. | Keep polling. |
queued | No | Accepted and queued for routing; waiting for the router to pick a lane. | Keep polling. |
routing | No | The router is selecting an eligible provider lane (or workflow-product lane) for your items. | Keep polling. |
dispatched | No | Work has been handed to the chosen provider lane(s) and is awaiting provider processing. | Keep polling. |
processing | No | The provider is actively running your items. This is usually the longest phase. | Keep polling, at a relaxed interval. |
completing | No | Provider work is done; BatchRouter is assembling results, billing, and delivery artifacts. | Keep polling. |
completed | Yes | All processing finished. Results are available. Note that individual items may still have failed — check per-item statuses. | Stop polling. Fetch results. |
failed | Yes | The batch could not complete (for example, no eligible lane, or an unrecoverable execution error). | Stop polling. Inspect the batch; retry if appropriate. |
cancelled | Yes | The batch was cancelled via POST /v1/batches/{batchId}/cancel. Work already dispatched may have completed before cancellation took effect. | Stop polling. |
expired | Yes | The batch did not complete within its SLA window, or its results were reclaimed by retention garbage collection. | Stop polling. Resubmit if you still need the work. |
Polling for a terminal status
Section titled “Polling for a terminal status”The recommended pattern is to poll GET /v1/batches/{batchId} until status is terminal, backing off between attempts so you do not poll a long-running batch too aggressively.
curl https://api.batchrouter.com/v1/batches/bat_123 \ -H "Authorization: Bearer $BATCHROUTER_API_KEY"async function pollUntilDone(batchId) { const terminal = new Set(['completed', 'failed', 'cancelled', 'expired']); while (true) { const res = await fetch(`https://api.batchrouter.com/v1/batches/${batchId}`, { headers: { Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}` }, }); const batch = await res.json(); if (terminal.has(batch.status)) return batch; await new Promise((r) => setTimeout(r, 30_000)); // back off between polls }}import os, time, requests
def poll_until_done(batch_id): terminal = {"completed", "failed", "cancelled", "expired"} headers = {"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}"} while True: res = requests.get( f"https://api.batchrouter.com/v1/batches/{batch_id}", headers=headers, ) batch = res.json() if batch["status"] in terminal: return batch time.sleep(30) # back off between pollsPer-item statuses
Section titled “Per-item statuses”A completed batch does not mean every item succeeded. The batch-level status describes the job as a whole; each item carries its own outcome. When you read results from GET /v1/batches/{batchId}/results, each row carries its own status (completed or failed) alongside an output (success payload) and an error (failure detail):
- A successful item has
status: "completed"and a populatedoutput; itserroris null. - A failed item has
status: "failed"and a populatederror; itsoutputis null.
So a terminal completed batch can contain a mix of succeeded and failed items. Always check items individually rather than assuming a completed batch means total success. For the exact result row fields, see the API reference.
If you point an existing openai SDK at the drop-in OpenAI-compatible surface (/v1/openai/v1/*), result rows are projected to OpenAI’s per-line shape instead — { id, custom_id, response: { status_code, request_id, body }, error } — where a populated response marks success and a populated error marks failure. That OpenAI projection is served by GET /v1/openai/v1/files/{fileId}/content, not by the native /results endpoint.
Reading results once terminal
Section titled “Reading results once terminal”Once a batch reaches completed, fetch the output:
-
Page through results with
GET /v1/batches/{batchId}/results(paginated), or -
Download the whole artifact via
GET /v1/batches/{batchId}/artifact-url, which returns a short-lived signed URL to the full output file.
If a batch later shows expired because retention garbage collection reclaimed its bytes, the artifact is gone. On the OpenAI-compatible download path, this is distinguishable: GET /v1/openai/v1/files/{fileId}/content returns a 410 for a reclaimed (expired) artifact — distinct from a never-existed 404. See Get your results for the full retrieval walkthrough.