Claude Opus 5 Batch API Tutorial: AI Contract Data Extraction at Scale, with Real Costs
A step-by-step tutorial for bulk AI document extraction with the Claude Opus 5 Batch API: structured outputs with JSON schemas, 50% batch pricing, the 300K output beta, and the real cost of processing 40,000 vendor contracts.
A procurement client came to us with 40,000 vendor contracts in a document store and a question their team had been answering one PDF at a time: which of these auto-renew in the next 90 days, at what price change, with what termination window? Classic bulk-extraction work, where model quality decides whether legal trusts the output and unit economics decide whether the project happens at all.
Claude Opus 5 launched this week at $5/$25 per million tokens, which is Opus 4.8 pricing for a model that trades blows with Fable 5 on benchmarks. Put it behind the Batch API's 50% discount and you're running a frontier model at $2.50/$12.50 per million tokens. That number moved this project from "pilot on a sample" to "run the whole corpus." Here's the full pipeline, as a tutorial you can adapt.
Step 1: make the JSON schema the contract
The most important design decision in bulk AI document extraction is that parsing must never fail on item 31,882. Claude Opus 5's structured outputs make the response schema enforced rather than requested. Pass output_config.format with a JSON schema and the first text block is guaranteed to be valid JSON matching it:
CONTRACT_SCHEMA = {
"type": "object",
"properties": {
"vendor_name": {"type": "string"},
"auto_renews": {"type": "boolean"},
"renewal_date": {"type": ["string", "null"], "format": "date"},
"price_change_pct": {"type": ["number", "null"]},
"termination_notice_days": {"type": ["integer", "null"]},
"governing_law": {"type": "string"},
"confidence": {"type": "string", "enum": ["high", "medium", "low"]},
"needs_human_review": {"type": "boolean"},
"review_reason": {"type": ["string", "null"]},
},
"required": ["vendor_name", "auto_renews", "renewal_date", "price_change_pct",
"termination_notice_days", "governing_law", "confidence",
"needs_human_review", "review_reason"],
"additionalProperties": False,
}
Two of those fields are process rather than data: confidence and needs_human_review. Opus 5 follows instructions literally, and told plainly ("if the renewal terms are ambiguous or reference an external amendment you cannot see, set needs_human_review to true and say why"), it actually does it instead of hallucinating a confident date. About 4% of our corpus came back flagged. That 4% is the product. It's what turns "AI did the contracts" into a review queue legal can staff.
Step 2: build the batch
The Claude Batch API takes up to 100,000 requests or 256 MB per batch, keyed by your custom_id. Everything the synchronous API supports works inside a batch, including structured outputs, PDFs, and prompt caching:
import anthropic
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request
client = anthropic.Anthropic()
SYSTEM = [
{"type": "text", "text": EXTRACTION_INSTRUCTIONS, # ~2K tokens, byte-stable
"cache_control": {"type": "ephemeral"}},
]
def make_request(doc_id: str, contract_text: str) -> Request:
return Request(
custom_id=doc_id,
params=MessageCreateParamsNonStreaming(
model="claude-opus-5",
max_tokens=4000,
output_config={
"effort": "medium",
"format": {"type": "json_schema", "schema": CONTRACT_SCHEMA},
},
system=SYSTEM,
messages=[{"role": "user", "content": contract_text}],
),
)
batch = client.messages.batches.create(
requests=[make_request(d.id, d.text) for d in chunk] # chunks of 10k docs
)
Two choices worth defending. effort: "medium": extraction against a schema isn't open-ended reasoning, and on our 200-document eval sample, medium matched high on field-level accuracy while spending about half the output tokens. Opus 5 defaults to high, so leaving effort unset on a 40,000-document batch is an expensive oversight. The shared system block with cache_control: identical instructions across every request get served from cache, and cache-read pricing stacks with the batch discount.
Step 3: poll, collect, key by custom_id
import time
while True:
batch = client.messages.batches.retrieve(batch.id)
if batch.processing_status == "ended":
break
time.sleep(120)
results, flagged, errored = {}, [], []
for result in client.messages.batches.results(batch.id):
if result.result.type == "succeeded":
msg = result.result.message
if msg.stop_reason == "refusal":
errored.append(result.custom_id) # quarantine, don't crash
continue
data = json.loads(next(b.text for b in msg.content if b.type == "text"))
(flagged if data["needs_human_review"] else results)[result.custom_id] = data
else:
errored.append(result.custom_id) # server-side error: resubmit
Results arrive in any order, so key by custom_id and never by position. Note the refusal check even here: Opus 5's safety classifiers apply inside batches too, and one weird scanned document shouldn't kill the run. Most batches complete well under the 24-hour ceiling; ours averaged around 40 minutes per 10K-document chunk.
Step 4 (optional): the 300K output beta for the final report
One launch-week addition specific to batches: with the output-300k-2026-03-24 beta header, Opus 5 supports up to 300K output tokens per request on the Batch API, versus 128K synchronous. We didn't need it for per-contract extraction. We used it for the final act instead: a single request that took the aggregated JSON for the worst 500 contracts and wrote the entire renewal-risk report, per-vendor narratives included, in one pass.
params=MessageCreateParamsNonStreaming(
model="claude-opus-5",
max_tokens=300000,
betas=["output-300k-2026-03-24"], # batch-only extended output
...
)
If you've ever built the "generate the report in 12 chunks and stitch them together" pipeline, you know which module this deletes.
What 40,000 contracts cost on the Claude Opus 5 Batch API
Final tally for the full corpus: roughly 610M input tokens (contracts average around 15K tokens each) and 41M output tokens. At batch rates, with the cached system prompt, the whole job came in just under $1,700 end to end, eval reruns included. The same job at synchronous Fable 5 prices would have been north of $8,000. Done by paralegals at market rates, six figures.
It ran over two nights, flagged 1,630 contracts for human review, and found 217 auto-renewals inside the 90-day window. Nine of those were renewing at price increases nobody had noticed, which paid for the project on their own.
The stack is three launch-week features composing cleanly: structured outputs so parsing never breaks, batch pricing so frontier quality fits the budget, and enough output headroom to write the deliverable in one pass. Bulk document intelligence has been technically possible for two years. What it got this week is a cost story you can put in front of a CFO.
Related reading
An independent comparison of the three agentic models that launched this month. Real pricing, production failure modes, cost per completed task, and the routing table we actually run — not a benchmark aggregation.
How to use the Claude Opus 5 effort parameter (low, medium, high, xhigh, max) to cut Claude API costs. Real per-request cost numbers, working Python code, and why we retired our Haiku/Sonnet/Opus routing layer.
A hands-on guide to building autonomous AI agents on the Claude Opus 5 API: the agent loop in Python, mid-conversation system messages, prompt caching costs, and what an overnight dependency-upgrade run actually cost.
Let's build something great.
Have a project in mind? We are an elite software and AI development studio ready to bring your ideas to production. Let's talk about your roadmap.