Complete end-to-end pipeline
On this page
Course 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 match 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.
Assembling the pieces
Previous lessons covered each step in isolation. This lesson assembles them into a complete pipeline, from data preparation to processing the results. You’ll end up with a script you can adapt to your use case.
Complete Python pipeline
import json
import time
from xai_sdk import Client
client = Client() # key read from XAI_API_KEY
# --- Step 1: prepare data ---
documents = [
{"id": f"doc-{i}", "text": f"Document content {i}..."}
for i in range(100)
]
# --- Step 2: write the JSONL 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 two sentences."},
{"role": "user", "content": doc["text"]},
],
"max_output_tokens": 200,
},
}
f.write(json.dumps(ligne, ensure_ascii=False) + "\n")
# --- Step 3: upload the file ---
with open("batch_requests.jsonl", "rb") as f:
fichier = client.files.upload(f, filename="batch_requests.jsonl")
# --- Step 4: create the batch from the file (it is now sealed) ---
batch = client.batch.create(batch_name="resumes_documents", input_file_id=fichier.id)
print(f"Batch created: {batch.batch_id}")
# --- Step 5: wait for processing to finish ---
while True:
batch = client.batch.get(batch_id=batch.batch_id)
etat = batch.state
if batch.cancel_by_xai_message:
raise RuntimeError(f"Batch canceled by xAI: {batch.cancel_by_xai_message}")
if etat.num_requests > 0 and etat.num_pending == 0:
break
print(f"{etat.num_success + etat.num_error}/{etat.num_requests} requests processed")
time.sleep(300) # every 5 minutes
# --- Step 6: retrieve results, page by page ---
resultats, echecs = {}, []
jeton = None
while True:
page = client.batch.list_batch_results(
batch_id=batch.batch_id, limit=100, pagination_token=jeton
)
for r in page.succeeded:
resultats[r.batch_request_id] = r.response.content
echecs.extend(page.failed)
if page.pagination_token is None:
break
jeton = page.pagination_token
with open("batch_results.json", "w", encoding="utf-8") as f:
json.dump(resultats, f, ensure_ascii=False, indent=2)
print(f"{len(resultats)} results, {len(echecs)} failures")
print("Total cost: $%.4f" % (batch.cost_breakdown.total_cost_usd_ticks / 1e10))
Three details matter. The stop condition requires num_requests > 0 because a batch created from a file is read in the background: right after creation, its counters may still all be zero. The cancel_by_xai_message field signals cancellation by xAI, for example due to an invalid line. And cost is expressed in ticks, ten-billionths of a dollar.
Step-by-step cURL pipeline
If you prefer the command line, here is the cURL equivalent (using jq to read responses):
# 1. Upload the JSONL file
FILE_ID=$(curl -s -X POST https://api.x.ai/v1/files \
-H "Authorization: Bearer $XAI_API_KEY" \
-F file="@batch_requests.jsonl" | jq -r '.id')
echo "File: $FILE_ID"
# 2. Create the batch from the file
BATCH_ID=$(curl -s -X POST https://api.x.ai/v1/batches \
-H "Authorization: Bearer $XAI_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"name\": \"resumes_documents\", \"input_file_id\": \"$FILE_ID\"}" | jq -r '.batch_id')
echo "Batch: $BATCH_ID"
# 3. Read counters
curl -s "https://api.x.ai/v1/batches/$BATCH_ID" \
-H "Authorization: Bearer $XAI_API_KEY" | jq '.state'
# 4. Retrieve the first page of results
curl -s "https://api.x.ai/v1/batches/$BATCH_ID/results?limit=100" \
-H "Authorization: Bearer $XAI_API_KEY" > results-page-1.json
Get subsequent pages by adding &pagination_token=… with the value returned by the previous page.
Error handling in production
A robust pipeline distinguishes three families of errors:
Submission errors: file upload or batch creation fails. Retry transient errors (timeout, rate limit, server error) with an increasing delay: 1 s, 2 s, 4 s, 8 s.
Processing errors: some requests fail within the batch. They appear in page.failed with their message; resubmit them, and only them, in a new batch.
Timeout and expiration: the 24-hour window is indicative, not guaranteed. What is unforgiving is expiration (expire_time): after this point, results are no longer accessible. Retrieve them as they come in rather than all at once at the end.
# Failed requests: identify them to resubmit in a new batch
for r in echecs:
print(f"Failure: {r.batch_request_id} — {r.error_message}")
a_relancer = {r.batch_request_id for r in echecs}
Cost optimization
Savings accumulate across four levers. Model choice comes first, by far the most powerful: at the standard rate as of November 2, 2026, grok-4.3 costs $1.25 per million input tokens and $2.50 for output, versus $2 and $6 for grok-4.7 (prompts under 200,000 tokens). For classification or short extraction, the lighter model often suffices, and the 20% batch discount applies to both. Response length comes next: max_output_tokens prevents paying for unnecessarily long outputs—for classification, a few tokens suffice. Prompts: removing ten tokens from a prompt repeated 50,000 times saves 500,000 input tokens. Finally, cache: xAI automatically caches identical request beginnings, and the batch discount also applies to tokens served from cache; so place common instructions at the start of each request, before the varying part.
Key takeaways
- A complete pipeline covers six steps: preparation, JSONL, upload, creation, monitoring, retrieval
- A batch from a file is read in the background: wait for
num_requests > 0before concluding it is finished - Handle the three error families: submission, processing, timeout and expiration
- Retrieve and save results as they come in, before
expire_time - Model choice, output length, short prompts, and common prefixes maximize savings