Skip to main content
Lesson 7 of 12 · Batch API: Bulk Processing

Complete end-to-end pipeline

Lesson 7 of 12 Updated November 2, 2026 Developer
On this page
  1. Assembling the pieces
  2. Complete Python pipeline
  3. Step-by-step cURL pipeline
  4. Error handling in production
  5. Cost optimization
  6. Key takeaways

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 > 0 before 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