Use the Message Batches API when responses do not need to be immediate: most batches finish in under 1 hour, the maximum is 24 hours, and everything costs 50 percent of standard pricing.
1. Decide batch vs realtime first. Batch fits when you need large volumes, do not need immediate responses, want cost efficiency, or are running large-scale evaluations. If any request needs an answer now, keep it on the synchronous Messages API.
2. Build requests inline. POST /v1/messages/batches with a requests array. Each entry has a custom_id and params that look exactly like a normal Messages API call (model, max_tokens, messages). custom_id must match ^[a-zA-Z0-9_-]{1,64}$ and must be unique within the batch, otherwise results cannot be matched back. A batch holds up to 100,000 requests or 256 MB. All Messages API features are supported inside batch params, including vision, tools, and caching.
3. Poll on processing_status. GET /v1/messages/batches/[batch-id] and wait for processing_status to read "ended". Do not read results before then: results_url only appears when processing is done. Track request_counts (processing, succeeded, errored, canceled, expired) to see progress.
4. Stream the JSONL results. Once ended, GET the results_url (or .../results). Entries stream as JSONL, unordered, shaped like {custom_id, result: {type: succeeded | errored | canceled | expired, message | error}}. Join on custom_id, never on order. Results are retained for 29 days after creation, so schedule your download: they expire.
5. Retry only the failures. Requests whose result type is errored can be rebuilt into a new batch with the same custom_ids. Do not resubmit the whole batch: succeeded requests already cost you once.
6. Log cost per batch. 50 percent pricing applies to all token usage in the batch, so record input/output tokens per custom_id from the succeeded messages to reconcile the bill.