Files & large batches
Most batches submit their work inline as an array of items. But two cases call for uploading a file first: a large JSONL request set that you’d rather not inline, and binary inputs (images, PDFs, audio) that a multimodal model needs to read. Both flow through POST /v1/files, which stores the bytes and returns a file_id you reference from your batch.
This guide covers the native file-upload endpoint, when to use a file versus inline items, and how to attach an image or document to an item.
When to use a file
Section titled “When to use a file”Use a file instead of inline items when either is true:
- The request set is large. Inlining thousands of items in one JSON body is awkward and memory-heavy on both ends. Upload the request set as a JSONL file and pass its id as the batch’s
input_file_id. See the JSONL format guide for the row shape. - An item needs a binary input. Images, PDFs, Office documents, audio, or video can’t live inline in JSON. Upload each one, then reference its
file_idinside an item’s content blocks.
Upload a file
Section titled “Upload a file”POST /v1/files takes a raw binary body — not multipart form data. You describe the file through headers:
| Header | Required | Purpose |
|---|---|---|
Content-Type | Yes | The actual MIME type of the body (e.g. image/png, application/pdf, application/json). |
Content-Length | Yes | Exact size in bytes, so the upload limit is enforced before the body is stored. |
X-BatchRouter-Filename | No | Original filename. URL-encode it if it contains spaces or non-ASCII characters. |
X-BatchRouter-Purpose | No | Upload purpose. Defaults to model_input. |
curl -X POST https://api.batchrouter.com/v1/files \ -H "Authorization: Bearer $BATCHROUTER_API_KEY" \ -H "Content-Type: image/png" \ -H "Content-Length: $(wc -c < ./chart.png)" \ -H "X-BatchRouter-Filename: chart.png" \ -H "X-BatchRouter-Purpose: model_input" \ --data-binary @./chart.pngimport { readFile } from 'node:fs/promises';
const bytes = await readFile('./chart.png');
const res = await fetch('https://api.batchrouter.com/v1/files', { method: 'POST', headers: { Authorization: `Bearer ${process.env.BATCHROUTER_API_KEY}`, 'Content-Type': 'image/png', 'Content-Length': String(bytes.byteLength), 'X-BatchRouter-Filename': 'chart.png', 'X-BatchRouter-Purpose': 'model_input', }, body: bytes,});
const { file_id } = await res.json();console.log(file_id); // file_...import osimport requests
with open("chart.png", "rb") as f: body = f.read()
res = requests.post( "https://api.batchrouter.com/v1/files", headers={ "Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}", "Content-Type": "image/png", "Content-Length": str(len(body)), "X-BatchRouter-Filename": "chart.png", "X-BatchRouter-Purpose": "model_input", }, data=body,)
file_id = res.json()["file_id"]print(file_id) # file_...A successful upload returns 201 with the file_id and file metadata:
{ "file_id": "file_9f3c2a...", "file": { "id": "file_9f3c2a...", "kind": "input_attachment", "content_type": "image/png", "size_bytes": 48210, "filename": "chart.png", "media_kind": "image", "created_at": "2026-06-18T10:21:00Z" }, "usage": { "image_block": { "type": "input_image", "file_id": "file_9f3c2a..." } }}The usage block is a ready-to-paste content block for the file’s media kind — image_block for images, file_block for documents — so you can drop it straight into an item.
Supported categories include image/*, audio/*, video/*, text/*, PDF, Word, PowerPoint, Excel, RTF, JSON, and XML. The endpoint returns 413 if the body exceeds the upload limit, 415 for an unsupported content type, and 411 if Content-Length is missing.
Reference an image or binary input in an item
Section titled “Reference an image or binary input in an item”For multimodal inputs, reference the file_id inside an item’s message content as an input_image (for images) or input_file (for documents and other binaries) block, alongside any text:
{ "customer_item_id": "item-1", "operation": "responses", "model": "gpt-4o-mini", "input": { "messages": [ { "role": "user", "content": [ { "type": "input_text", "text": "Describe the trend in this chart." }, { "type": "input_image", "file_id": "file_9f3c2a..." } ] } ] }}For a PDF or document, use an input_file block (which may carry an optional filename):
{ "type": "input_file", "file_id": "file_9f3c2a...", "filename": "report.pdf" }These content blocks work in both inline items and JSONL rows uploaded as an input_file_id.
Submit a large request set as a file
Section titled “Submit a large request set as a file”When the requests themselves are numerous, upload them as a JSONL file (one item per line) and pass the returned id as the batch’s input_file_id instead of inline items.
-
Build the JSONL. One batch item per line, in the JSONL format. Mixed-model JSONL is supported — BatchRouter keeps one customer batch and splits execution into model-specific lanes.
-
Upload it to
POST /v1/fileswithContent-Type: application/json(the same upload call shown above), and keep the returnedfile_id. -
Create the batch with
input_file_idset to that id — noitemsfield:Terminal window curl -X POST https://api.batchrouter.com/v1/batches \-H "Authorization: Bearer $BATCHROUTER_API_KEY" \-H "Content-Type: application/json" \-H "Idempotency-Key: $(uuidgen)" \-d '{"input_file_id": "file_9f3c2a...","sla_tier": "standard","routing_mode": "cheapest"}'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}`,'Content-Type': 'application/json','Idempotency-Key': randomUUID(),},body: JSON.stringify({input_file_id: 'file_9f3c2a...',sla_tier: 'standard',routing_mode: 'cheapest',}),});const batch = await res.json();console.log(batch.id); // bat_...import osimport uuidimport requestsres = requests.post("https://api.batchrouter.com/v1/batches",headers={"Authorization": f"Bearer {os.environ['BATCHROUTER_API_KEY']}","Content-Type": "application/json","Idempotency-Key": str(uuid.uuid4()),},json={"input_file_id": "file_9f3c2a...","sla_tier": "standard","routing_mode": "cheapest",},)batch = res.json()print(batch["id"]) # bat_...
From here the flow is identical to inline submission: the batch returns 202 with a bat_... id, and you poll and fetch results as usual. See Submit a batch for the full lifecycle.