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

JSONL format and upload via the Files API

Lesson 6 of 12 Updated November 2, 2026 Developer
On this page
  1. Bulk processing with JSONL
  2. JSONL file structure
  3. Generate a JSONL file in Python
  4. Upload via the Files API
  5. Create the batch from the file
  6. JSONL format limits
  7. Generate a JSONL file in JavaScript
  8. Validation and debugging
  9. Key takeaways

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 the batch_request_id in the results
  • method: always "POST"
  • url: the target endpoint, one of the seven accepted
  • body: 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_id per 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