Right, let’s talk about moving beyond one-off chat completions. The Files API and batch processing are where you stop asking Claude to solve a single riddle and start putting it on a factory line to solve a thousand. It’s the difference between a scalpel and a conveyor belt of scalpel-wielding robots. The core idea is brutally simple: you upload your data, you tell Claude what to do with each piece of it, and you get back a neatly packaged JSONL file with all the results. No more babysitting individual API calls.

The Unsexy (But Critical) File Upload

First, you need to get your data into Claude’s world. You don’t just send a text string; you upload a file to their servers and get back an ID. This file has to be in JSON Lines format (.jsonl), which is basically a text file where each line is its own self-contained JSON object. This is non-negotiable. Why? Because it’s efficient, streamable, and prevents one malformed JSON blob from tanking your entire batch job.

Here’s how you upload the thing. Notice the purpose is set to "batch". This is a crucial flag that tells the API “hey, this isn’t a fine-tuning dataset or something else; this is for the batch endpoint.”

import anthropic
import json

client = anthropic.Anthropic(api_key="YOUR_KEY")

# Create a sample .jsonl file
data = [
    {"custom_id": "article_1", "params": {"model": "claude-3-sonnet-20240229", "messages": [{"role": "user", "content": "Summarize this article for a 5th grader: The geopolitical implications of quantum entanglement are often overstated..."}]}},
    {"custom_id": "product_2", "params": {"model": "claude-3-haiku-20240307", "messages": [{"role": "user", "content": "Write a catchy product description for this: A self-cleazing coffee mug."}]}}
]

with open("batch_input.jsonl", "w") as f:
    for item in data:
        f.write(json.dumps(item) + "\n")

# Upload it
with open("batch_input.jsonl", "rb") as f:
    file_obj = client.files.create(file=f, purpose="batch")

print(f"File uploaded with ID: {file_obj.id}")
# Output: File uploaded with ID: file-abc123...

Kicking Off the Batch Job

Now for the main event. You take that file ID and you pass it to the batch endpoint. You’ll get back a batch object immediately, but its status will be validating. Claude’s infrastructure is now queuing up your job and making sure your file isn’t, well, broken.

batch = client.batches.create(
    input_file_id=file_obj.id,
    endpoint="/v1/messages",
    completion_window="24h"
)

print(f"Batch ID: {batch.id}")
print(f"Status: {batch.status}") # Probably 'validating'

The completion_window is your SLA. “24h” is the standard, and it’s plenty fast—your job will usually complete in minutes, not hours. But this is their way of saying “We promise it’ll be done within this timeframe, so don’t poll us every 10 seconds.”

The Waiting Game and Retrieving Results

This is the part where you learn patience. You need to poll the API to check if the batch is completed. Do not, I repeat, do not set up a tight loop that checks every second. You’ll hit rate limits and annoy everyone. Once every 30 or 60 seconds is plenty.

import time

while True:
    batch_status = client.batches.retrieve(batch.id)
    if batch_status.status == "completed":
        print("Batch processing complete!")
        break
    elif batch_status.status in ["failed", "expired", "cancelled"]:
        print(f"Batch failed with status: {batch_status.status}")
        break
    else:
        print(f"Status is still: {batch_status.status}. Waiting...")
        time.sleep(30) # Be polite.

# Now, download the results!
if batch_status.output_file_id:
    results_content = client.files.content(batch_status.output_file_id)
    with open("batch_output.jsonl", "wb") as f:
        f.write(results_content.content)

The Output: What You Actually Get

Open that batch_output.jsonl file. Each line will correspond to an input line. The structure is robust, which is a fancy way of saying “verbose.” You’ll get the full content array, usage data, and the type of response. Most importantly, it includes your custom_id, which is your only lifeline for matching results to inputs. This is why you use descriptive custom IDs and not just sequential numbers. If you used "article_1", you can now easily find that result.

{
  "custom_id": "article_1",
  "response": {
    "id": "msg_123",
    "model": "claude-3-sonnet-20240229",
    "content": [{"type": "text", "text": "Okay, so imagine two tiny particles..."}],
    "usage": {"input_tokens": 100, "output_tokens": 50}
  },
  "error": null
}

Pitfalls and the “Oh Crap” Moments

Let’s be direct about where this can go wrong.

  • Bad JSONL: The number one cause of failure. Your file must be valid JSONL. Not JSON, not CSV masquerading as JSONL. Each line must be a valid JSON object. Validate it with a tool like jq (jq . < your_file.jsonl) before uploading.
  • The Silent Error: The batch job will complete successfully even if individual items inside it fail. You must check response.error for each line in your output. A successful batch means the processing didn’t fail, not that every single request succeeded.
  • Cost Ambush: You are charged for the entire batch the moment it starts processing. There’s no “oh, cancel it!” if you see a mistake. Double-check your input file and your code before you hit create. It’s surprisingly easy to accidentally queue up 10,000 requests instead of 100.
  • Tool Use Limitation: As of this writing, you cannot use the tools parameter in a batch request. This is a huge, glaring omission and a massive pain point. The designers made a questionable choice here, prioritizing stability for the core chat functionality first. For now, if you need batch tool use, you’re stuck building your own parallel processing loop, which is less efficient and more painful. We all complain about it. You should too.

The Batch API is a workhorse. It’s for when you have a terabyte of text to analyze, a million product descriptions to rewrite, or a thousand documents to summarize. It’s not for casual experimentation. Use it wisely, validate your inputs religiously, and always check for those per-line errors. Now go automate something.