JSONL format and upload via the Files API
On this page
Updated November 2, 2026 based on xAI documentation: the batch discount is 20% on eligible text models (not 50%), examples follow the REST API and the official
xai-sdk(client.batch.*; xAI’s Batch API does not use OpenAI’s format, so the OpenAI SDK does not apply), and the Responses API takes precedence over Chat Completions, a legacy endpoint at xAI. Sources: Batch API · REST reference · Batch pricing.
Bulk processing with JSONL
For large volumes prepared in advance, a JSONL file is the simplest route. You write all your requests into a file, upload it through the Files API, then create the batch from that file. A single file can hold up to 50,000 requests.
JSONL file structure
A JSONL (JSON Lines) file contains one JSON object per line. Each line represents a complete request with four fields:
{"custom_id": "req-1", "method": "POST", "url": "/v1/responses", "body": {"model": "grok-4.3", "input": [{"role": "user", "content": "Summarize this text in one sentence: ..."}]}}
{"custom_id": "req-2", "method": "POST", "url": "/v1/responses", "body": {"model": "grok-4.3", "input": [{"role": "user", "content": "Classify this ticket: bug, feature, or question. ..."}]}}
{"custom_id": "req-3", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "grok-4.3", "messages": [{"role": "user", "content": "Translate into English: ..."}]}}
custom_id: a unique identifier within the file; it becomes thebatch_request_idin the resultsmethod: always"POST"url: the target endpoint, one of the seven acceptedbody: the request body, matching the API reference for that endpoint
Each line must be valid, self-contained JSON: no separating comma, no enclosing bracket. Endpoints can be mixed in the same file; each request is routed independently.
Generate a JSONL file in Python
import json
documents = [
{"id": "doc-001", "text": "First article to summarize..."},
{"id": "doc-002", "text": "Second article to summarize..."},
# ... up to 50,000 documents per file
]
with open("batch_requests.jsonl", "w", encoding="utf-8") as f:
for doc in documents:
ligne = {
"custom_id": doc["id"],
"method": "POST",
"url": "/v1/responses",
"body": {
"model": "grok-4.3",
"input": [
{"role": "system", "content": "Summarize in three sentences."},
{"role": "user", "content": doc["text"]},
],
},
}
f.write(json.dumps(ligne, ensure_ascii=False) + "\n")
Upload via the Files API
You upload the file through the /v1/files endpoint, with no other parameters:
curl -X POST https://api.x.ai/v1/files \
-H "Authorization: Bearer $XAI_API_KEY" \
-F file="@batch_requests.jsonl"
With the Python SDK:
from xai_sdk import Client
client = Client()
with open("batch_requests.jsonl", "rb") as f:
fichier = client.files.upload(f, filename="batch_requests.jsonl")
print(f"Uploaded file: {fichier.id}")
Create the batch from the file
The file is not added to an existing batch: you specify it at creation time with input_file_id:
curl -X POST https://api.x.ai/v1/batches \
-H "Authorization: Bearer $XAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "article_summaries", "input_file_id": "file-abc123"}'
batch = client.batch.create(batch_name="article_summaries", input_file_id=fichier.id)
print(f"Batch created: {batch.batch_id}")
The file is then read in the background. If even one line is invalid, the batch is canceled with an error message. Tracking and retrieving results then work exactly as for a batch submitted through the API.
JSONL format limits
- 50,000 requests maximum per file
- 200 MB maximum per file
- a unique
custom_idper line - a batch created from a file is sealed: you cannot add to it later through the API
If your batch exceeds these limits, split it into multiple files and create one batch per file. There is no cap on the number of batches per team.
Generate a JSONL file in JavaScript
import fs from "fs";
const documents = [
{ id: "doc-001", text: "First article..." },
{ id: "doc-002", text: "Second article..." },
];
const lines = documents.map(doc => JSON.stringify({
custom_id: doc.id,
method: "POST",
url: "/v1/responses",
body: {
model: "grok-4.3",
input: [
{ role: "user", content: `Summarize: ${doc.text}` }
]
}
}));
fs.writeFileSync("batch_requests.jsonl", lines.join("\n") + "\n");
Validation and debugging
One invalid line cancels the entire batch: validate the file before uploading it. A simple script covers the essentials:
import json
import os
ENDPOINTS = {
"/v1/responses", "/v1/chat/completions",
"/v1/images/generations", "/v1/images/edits",
"/v1/videos/generations", "/v1/videos", "/v1/videos/edits", "/v1/videos/extensions",
}
def valider_jsonl(chemin: str) -> list[str]:
erreurs, ids = [], set()
if os.path.getsize(chemin) > 200 * 1024 * 1024:
erreurs.append("file larger than 200 MB")
with open(chemin, encoding="utf-8") as f:
for i, ligne in enumerate(f, 1):
try:
obj = json.loads(ligne)
except json.JSONDecodeError as e:
erreurs.append(f"line {i}: invalid JSON ({e})")
continue
cid = obj.get("custom_id")
if not cid or cid in ids:
erreurs.append(f"line {i}: missing or duplicate custom_id")
ids.add(cid)
if obj.get("method") != "POST" or obj.get("url") not in ENDPOINTS:
erreurs.append(f"line {i}: invalid method or url")
if not isinstance(obj.get("body"), dict):
erreurs.append(f"line {i}: missing body")
if len(ids) > 50_000:
erreurs.append("more than 50,000 requests")
return erreurs
print(valider_jsonl("batch_requests.jsonl") or "File is valid")
Key takeaways
- The JSONL format contains one JSON object per line:
custom_id,method,url,body - You upload the file through
/v1/files, then specify it when creating the batch (input_file_id) - Maximum 50,000 requests and 200 MB per file; beyond that, one batch per file
- A batch created from a file is sealed, and one invalid line cancels it entirely
- Validate the file before uploading