Claude API: The Complete Developer Guide
What is Claude API?
Claude API is Anthropic's programmatic interface for integrating Claude models into applications. It runs through the Messages API: you send an array of messages (user/assistant), you get the model's response back. It supports text, vision, tool use, streaming, adaptive thinking, and prompt caching. Available via REST API, the Python SDK, and the TypeScript SDK.
TL;DR
- -Claude API runs through the Messages API - a single endpoint for text, images, tool use, streaming, and thinking
- -Four current models: Opus 5.5 ($4/$20 per MTok) for agents and hard tasks, Sonnet 5.5 ($2/$10) as the speed-quality balance, Haiku 5.5 (from $0.10/$0.50) for high-volume operations, Fable 5.1 ($10/$50) for the most demanding reasoning
- -Tool use turns Claude into an executor: the model generates JSON calls to your functions, you return the result - the loop repeats until the task is done
- -A prompt cache read costs 10% of the base input price (5% on Opus 5.5 and Sonnet 5.5, 2.5% on Fable 5.1); the Batches API gives a 50% discount on non-urgent work
- -On current models thinking is on by default and its depth is set with `output_config.effort` (low/medium/high/xhigh/max); `budget_tokens`, `temperature`/`top_p`/`top_k`, and assistant prefill return a 400
Claude API is the programmatic interface for working with Anthropic’s models. One endpoint - the Messages API - covers text, images, tool use, streaming, and thinking. Python SDK, TypeScript SDK, raw REST - three ways to integrate.
This article is a complete reference. From the first request to advanced techniques: prompt caching, batches, vision. Every example is working code you can copy and run.
If Claude sits behind an endpoint in your product, define that public contract separately. The REST API design guide covers naming, errors, versioning, and OpenAPI generation for the layer your clients call.
What Claude API Is
The Messages API - a single endpoint
All work with Claude goes through one endpoint: POST https://api.anthropic.com/v1/messages. You send an array of messages with user and assistant roles, and the model generates the next message in the conversation.
This isn’t a GPT-3-style completions API - the Messages API is built around conversational structure. Every request carries the full conversation history. No state is kept between calls.
Models
Four current models (as of November 2026):
| Model | API ID | Context | Max output | Price (input/output per MTok) |
|---|---|---|---|---|
| Fable 5.1 | claude-fable-5-1 | 1M | 128k | $10 / $50 |
| Opus 5.5 | claude-opus-5-5 | 1M | 128k | $4 / $20 |
| Sonnet 5.5 | claude-sonnet-5-5 | 1M | 128k | $2 / $10 |
| Haiku 5.5 | claude-haiku-5-5 | 1M | 128k | $0.10 / $0.50 for prompts up to 100K tokens, $0.50 / $2.50 above |
Opus 5.5 - the main flagship, built for long-running agentic coding and knowledge work. Anthropic’s docs suggest starting with it for most workloads. Its default effort is medium, so set the level explicitly when a task needs more depth.
Sonnet 5.5 - the workhorse: the best combination of speed and intelligence for everyday coding, agent, and enterprise workloads. Half the price of Opus 5.5 on both input and output ($2/$10 vs $4/$20).
Haiku 5.5 - speed and economy. Classification, extraction, routing, subagent tasks. On prompts up to 100K tokens it is 20x cheaper than Sonnet 5.5; above that threshold every price for the request is 5x higher.
Fable 5.1 - the most capable model in the lineup, for demanding reasoning and long-horizon agentic work. The slowest and most expensive; reach for it when Opus 5.5 at higher effort still falls short on your evals.
Opus 5, Sonnet 5, Haiku 4.5, Fable 5, and the Opus/Sonnet 4.x models are now legacy: still available, with retirement dates listed on the model deprecations page. Coming from Sonnet 4.6, Haiku 4.5, or older models, re-count your tokens: Claude 4.7 and later models use a newer tokenizer that produces roughly 1x to 1.35x as many tokens for the same text (about 30% more on average, per the pricing page).
How it differs from Claude.ai
Claude.ai is a chat interface. The API gives you what chat doesn’t:
- System prompts - precise control over model behavior
- Tool use - the model calls your functions
- Streaming - token-by-token output
- Thinking - a summary of the model’s reasoning and control over its depth via
effort - Prompt caching - caching of repeated context
- Batches - asynchronous batch processing at a 50% discount
- Full control - effort, stop sequences, metadata
Quick Start
Getting an API key
- Sign up for the Claude Console at platform.claude.com
- Go to Settings → API Keys
- Create a key
- Add credits under Billing
The key is tied to a workspace. One workspace - one set of keys, limits, and billing.
For local development you can skip the key: install the ant CLI and run ant auth login - the SDKs pick up those credentials automatically.
Installing the SDK
Python:
pip install anthropic
TypeScript:
npm install @anthropic-ai/sdk
The examples assume the current major versions (anthropic 2.x for Python, @anthropic-ai/sdk 1.x for TypeScript). They send API version 2026-10-21 by default.
The SDK picks up the key from the ANTHROPIC_API_KEY environment variable:
export ANTHROPIC_API_KEY="your-api-key"
The first request
Python:
import anthropic
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
messages=[
{"role": "user", "content": "Explain the difference between REST and GraphQL in three sentences."}
]
)
for block in message.content:
if block.type == "text":
print(block.text)
TypeScript:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const message = await client.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 4096,
messages: [
{ role: "user", content: "Explain the difference between REST and GraphQL in three sentences." }
]
});
for (const block of message.content) {
if (block.type === "text") {
console.log(block.text);
}
}
The response lives in message.content - an array of content blocks. On current models thinking is on by default, so one or more thinking blocks can come before the text block. Pick blocks by type, not by position: code like content[0].text breaks on these responses.
Streaming - real-time output
Streaming delivers tokens as they’re generated - the user sees output immediately.
Python:
import anthropic
client = anthropic.Anthropic()
with client.messages.stream(
model="claude-sonnet-5-5",
max_tokens=4096,
messages=[
{"role": "user", "content": "Write a binary search function in Python."}
]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
TypeScript:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const stream = client.messages.stream({
model: "claude-sonnet-5-5",
max_tokens: 4096,
messages: [
{ role: "user", content: "Write a binary search function in TypeScript." }
]
});
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
process.stdout.write(event.delta.text);
}
}
Messages API: a full breakdown
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Model ID: claude-opus-5-5, claude-sonnet-5-5, claude-haiku-5-5, claude-fable-5-1 |
messages | array | Yes | Array of messages with user and assistant roles |
max_tokens | integer | Yes | Hard ceiling on output, thinking included (current models: up to 128k; the SDK requires streaming above 21,333) |
system | string/array | No | System prompt - instructions for the model |
thinking | object | No | Thinking mode: {"type": "adaptive"} is the default on current models; display controls what the thinking blocks contain |
output_config | object | No | effort (reasoning depth and token spend) and format (structured outputs) |
temperature, top_p, top_k | - | - | Don’t send them: non-default values return a 400 on the 5.5 models and Fable 5.1, and starting with API version 2026-10-21 any value returns a 400 on every model. The current SDK major versions no longer have these parameters |
stop_sequences | array | No | Strings that stop generation when produced |
stream | boolean | No | Streamed output via Server-Sent Events |
metadata | object | No | user_id for tracking and abuse detection |
System prompt
The system prompt sets the role and behavior rules. It’s passed as a separate parameter, not through messages.
Python:
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
system="You are a senior Python developer. Answer concisely. Code should include types and docstrings.",
messages=[
{"role": "user", "content": "Write a retry decorator with exponential backoff."}
]
)
The system prompt is a string or an array of content blocks. The array form is needed for prompt caching:
system=[
{
"type": "text",
"text": "You are a senior Python developer. Answer concisely.",
"cache_control": {"type": "ephemeral"}
}
]
Multi-turn conversations
Claude doesn’t keep state between requests. To continue a conversation, send the full history:
Python:
messages = [
{"role": "user", "content": "What is dependency injection?"},
{"role": "assistant", "content": "Dependency injection is a pattern where an object's dependencies are supplied from the outside instead of being created internally..."},
{"role": "user", "content": "Show an example in Python using a dataclass."}
]
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
messages=messages
)
Each message is an object with role (user or assistant) and content. Roles alternate. Two consecutive messages from the same role get merged by the API. The last message must not be an assistant turn: Claude 4.6 and later models reject a prefilled assistant response with a 400.
Stop sequences
Custom strings that stop generation when produced:
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
stop_sequences=["```", "END"],
messages=[
{"role": "user", "content": "Write an nginx JSON configuration."}
]
)
# message.stop_reason == "stop_sequence"
# message.stop_sequence == "```"
Useful for structured output: the model generates up to a specific marker, and you parse the result.
Metadata
user_id in metadata is used for abuse tracking. Add it in production:
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
metadata={"user_id": "user_abc123"},
messages=[
{"role": "user", "content": "Hello!"}
]
)
Use hashes or UUIDs - don’t pass email addresses or real names.
Tool Use (Function Calling)
Tool use is how you call your own functions from Claude. You describe tools via JSON Schema. The model decides when and which one to call, and constructs the parameters. You execute the call and return the result.
Defining tools
A tool is a name, a description, and a JSON Schema for its input parameters:
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a given city.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g.: Moscow"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["city"]
}
}
]
Descriptions matter. Claude reads them to decide whether to call a tool. The more precise the description, the better the choice.
The tool_use → tool_result loop
The loop looks like this:
- You send a request with
tools - Claude returns a
tool_useblock with the tool name and parameters - You execute the call
- You send back the assistant message unchanged (including its
thinkingblocks) plus the result as atool_result - Claude produces the final answer (or calls the next tool)
Step 4 matters on current models: the API rejects edited, reordered, or partially dropped thinking blocks with a 400. Append response.content as is, without filtering blocks by type.
Full example: a weather bot
Python:
import anthropic
import json
client = anthropic.Anthropic()
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a given city.",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City"},
"units": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}
]
def get_weather(city: str, units: str = "celsius") -> dict:
"""Stub — in production this would call a real API."""
return {"city": city, "temperature": 22, "units": units, "condition": "cloudy"}
# Step 1: send the request
messages = [{"role": "user", "content": "What's the weather in Moscow?"}]
response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
tools=tools,
messages=messages
)
# Step 2: handle tool_use
if response.stop_reason == "tool_use":
# Find the tool_use block in the response
tool_block = next(b for b in response.content if b.type == "tool_use")
# Execute the call
result = get_weather(**tool_block.input)
# Step 3: return the result (the assistant message goes back unchanged, thinking blocks included)
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_block.id,
"content": json.dumps(result)
}
]
})
# Step 4: get the final answer
final_response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
tools=tools,
messages=messages
)
for block in final_response.content:
if block.type == "text":
print(block.text)
TypeScript:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [
{
name: "get_weather",
description: "Get the current weather for a given city.",
input_schema: {
type: "object" as const,
properties: {
city: { type: "string", description: "City" },
units: { type: "string", enum: ["celsius", "fahrenheit"] }
},
required: ["city"]
}
}
];
function getWeather(city: string, units = "celsius") {
return { city, temperature: 22, units, condition: "cloudy" };
}
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: "What's the weather in Moscow?" }
];
const response = await client.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 4096,
tools,
messages
});
if (response.stop_reason === "tool_use") {
const toolBlock = response.content.find(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use"
)!;
const result = getWeather(
(toolBlock.input as any).city,
(toolBlock.input as any).units
);
messages.push({ role: "assistant", content: response.content });
messages.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: toolBlock.id,
content: JSON.stringify(result)
}
]
});
const finalResponse = await client.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 4096,
tools,
messages
});
for (const block of finalResponse.content) {
if (block.type === "text") {
console.log(block.text);
}
}
}
On Opus 5.5 and Sonnet 5.5, the short notes the model writes between tool calls come back as thinking blocks rather than text, and they are empty by default. If your UI shows that progress, set thinking: {"type": "adaptive", "display": "summarized"} (or display: "updates" with the beta header thinking-display-updates-2026-08-18).
tool_choice - controlling tool invocation
| Value | Behavior |
|---|---|
{"type": "auto"} | The model decides on its own - call or not (default) |
{"type": "any"} | The model must call at least one tool. Returns a 400 on Opus 5.5, Sonnet 5.5, and Fable 5.1 |
{"type": "tool", "name": "get_weather"} | The model must call a specific tool. Returns a 400 on Opus 5.5, Sonnet 5.5, and Fable 5.1 |
{"type": "none"} | Tools are disabled |
Forced tool use still works on Haiku 5.5 and the legacy models:
# Forcing a specific tool call (Haiku 5.5)
response = client.messages.create(
model="claude-haiku-5-5",
max_tokens=4096,
tools=tools,
tool_choice={"type": "tool", "name": "get_weather"},
messages=[{"role": "user", "content": "Tell me about Moscow."}]
)
On Opus 5.5, Sonnet 5.5, and Fable 5.1, use tool_choice: {"type": "auto"}, name the tool in the prompt, and set strict: true on the tool so its arguments always match the schema. If all you need is JSON in a given structure, use structured outputs.
Parallel tool calls
Claude can call several tools in a single turn. The response contains multiple tool_use blocks - handle all of them and return all the results:
# If Claude called 3 tools — return 3 results
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
To disable parallel calls: tool_choice: {"type": "auto", "disable_parallel_tool_use": true}.
Extended Thinking
Thinking lets Claude reason before answering. The model breaks the problem into steps, checks hypotheses, catches mistakes - and only then answers. The API never returns the raw chain of thought: at most you get a summary of it.
When to turn it on
- Complex math and logic problems
- Multi-step code analysis
- Tasks with a non-obvious solution
- Any situation where “think longer” means “answer better”
On the current lineup the question is less “turn it on” and more “how deep”: thinking is on by default, and you tune it with effort.
Parameters
With the release of Claude 4.6, Anthropic moved to adaptive thinking - the model decides for itself how much to think. The manual mode thinking: {type: "enabled", budget_tokens: N} returns a 400 Bad Request on every current model.
What each current model does with the thinking field:
| Model | Without thinking | Turning thinking off | Default effort |
|---|---|---|---|
| Opus 5.5 | adaptive | Not possible: {"type": "disabled"} returns a 400 | medium |
| Fable 5.1 | adaptive | Not possible: {"type": "disabled"} returns a 400 | high |
| Sonnet 5.5 | adaptive | disabled returns a 400; {"type": "between_tools"} turns off up-front thinking at effort high or below | high |
| Haiku 5.5 | adaptive | {"type": "disabled"} at effort high or below | medium |
All four accept all five effort levels: low, medium, high, xhigh, max. Depth is controlled via output_config.effort:
response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=16000,
thinking={"type": "adaptive", "display": "summarized"},
output_config={"effort": "high"}, # low | medium | high | xhigh | max
messages=[
{"role": "user", "content": "Prove that there are infinitely many primes p where p mod 4 == 3."}
]
)
effort is a soft signal, not a hard token limit. The hard limit is max_tokens, and it covers thinking plus answer text. To cut cost or latency on a thinking workload, lower effort first; to give the model more depth, raise it. On simple requests Claude may skip thinking entirely - then the response has no thinking block at all.
Reading thinking blocks
The response contains thinking and text blocks:
for block in response.content:
if block.type == "thinking":
print(f"Reasoning: {block.thinking}")
elif block.type == "text":
print(f"Answer: {block.text}")
On every current model (Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5) the default is display: "omitted": the block comes back with an empty thinking field and only a signature. On Opus 4.6, Sonnet 4.6, and earlier models the default was display: "summarized". To see the gist of the reasoning, set thinking: {type: "adaptive", display: "summarized"}, as in the example above. display: "omitted" speeds up the first text token when streaming. Billing covers the full thinking tokens regardless of the display value.
Streaming with extended thinking
Python:
with client.messages.stream(
model="claude-sonnet-5-5",
max_tokens=16000,
thinking={"type": "adaptive", "display": "summarized"},
output_config={"effort": "high"},
messages=[{"role": "user", "content": "Implement a red-black tree in Python."}],
) as stream:
for event in stream:
if event.type == "content_block_delta":
if event.delta.type == "thinking_delta":
print(event.delta.thinking, end="", flush=True)
elif event.delta.type == "text_delta":
print(event.delta.text, end="", flush=True)
Without display: "summarized", the thinking_delta events arrive with an empty string, and the user sees a pause before the answer.
Limitations and compatibility
- Don’t send
temperature,top_p, ortop_k: on Fable 5.1 and the 5.5 models non-default values return a 400, and starting with API version2026-10-21any value does - No prefill: Claude 4.6 and later models reject a prefilled final assistant message with a 400
- Forced tool use (
tool_choiceany/tool) returns a 400 on Opus 5.5, Sonnet 5.5, and Fable 5.1 max_tokenscovers thinking plus answer; ateffort: xhighormax, start from 64k- With
max_tokensabove 21,333 the SDK requires streaming: use.stream()and.get_final_message() - For very long outputs, the Batches API offers up to 300k tokens (see below)
Cost
Thinking tokens are billed as output tokens, even when the thinking text isn’t returned to you. The cost depends on the amount actually spent (effort steers it, but doesn’t cap it). usage.output_tokens is the billed number, and it includes thinking, so it won’t match the length of the visible text.
The display: "omitted" option hides thinking blocks in the response and reduces latency, but not cost - the tokens are still billed either way.
Prompt Caching
Prompt caching preserves processed tokens across requests. A cache read costs 10% of the base input price on most models, 5% on Opus 5.5 and Sonnet 5.5, and 2.5% on Fable 5.1. A comparison with OpenAI, Gemini and DeepSeek and a break-even formula are in how prompt caching works across providers.
How it works
You mark blocks with a cache_control flag. Anthropic caches the result. The next request with the same content is a cache hit - the tokens aren’t re-processed.
The cache is hierarchical: tools → system → messages. A change at any level invalidates it and everything after it. Changing the thinking settings or the effort level also starts a new cache prefix.
TTL: two options
| TTL | Cache write | Cache read | When to use |
|---|---|---|---|
| 5 minutes (default) | 1.25x base price | 0.1x base price (0.05x on Opus 5.5 and Sonnet 5.5, 0.025x on Fable 5.1) | Frequent requests (more often than every 5 minutes) |
| 1 hour | 2x base price | Same as above | Agentic tasks, long sessions |
Minimum size for caching
| Model | Minimum tokens |
|---|---|
| Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5, Opus 5 | 512 |
| Sonnet 5, Opus 4.8, Sonnet 4.6 | 1024 |
| Haiku 4.5 | 4096 |
Content shorter than the minimum won’t get cached - the request still processes without errors, just without a cache.
Example: caching a large document
Python:
import anthropic
client = anthropic.Anthropic()
# A large document — cache it in the system prompt
long_document = open("technical_spec.md").read()
response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
system=[
{
"type": "text",
"text": "You are a technical analyst. Answer questions about the document below.",
},
{
"type": "text",
"text": long_document,
"cache_control": {"type": "ephemeral"} # 5-minute cache
}
],
messages=[
{"role": "user", "content": "What non-functional requirements are described in the document?"}
]
)
# Check the cache
usage = response.usage
print(f"Cache write: {usage.cache_creation_input_tokens}")
print(f"Cache read: {usage.cache_read_input_tokens}")
print(f"Uncached: {usage.input_tokens}")
A second request with the same system prompt is a cache hit - cache_read_input_tokens will be non-zero.
1-hour cache
system=[
{
"type": "text",
"text": long_document,
"cache_control": {"type": "ephemeral", "ttl": "1h"}
}
]
Use the 1-hour cache in agentic scenarios where more than 5 minutes pass between requests.
Automatic caching
There’s no caching by default - without cache_control, nothing gets cached. The simplest option, which Anthropic’s docs recommend as the starting point, is a single top-level cache_control on the request: the API places the breakpoint on the last cacheable block itself and moves it forward as the conversation grows. It takes one of the 4 available breakpoint slots.
Vision (Multimodality)
Claude accepts images in messages. Two ways: base64 and URL.
Supported formats
JPEG, PNG, GIF, WebP (for animations, only the first frame is used). Maximum size - 10 MB per image (base64-encoded) on the Claude API, 5 MB on Amazon Bedrock. Maximum dimensions - 8000x8000 px.
Base64
Python:
import anthropic
import base64
client = anthropic.Anthropic()
with open("screenshot.png", "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
messages=[
{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": image_data
}
},
{
"type": "text",
"text": "Analyze this UI screenshot. Find UX problems."
}
]
}
]
)
for block in message.content:
if block.type == "text":
print(block.text)
URL
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
messages=[
{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "url",
"url": "https://example.com/diagram.png"
}
},
{
"type": "text",
"text": "Describe this architecture diagram."
}
]
}
]
)
Token count for images
Claude processes an image in 28×28-pixel patches: it costs ⌈width / 28⌉ × ⌈height / 28⌉ tokens. Larger images are downscaled first. All current models are in the high-resolution tier (Claude 4.7 and later): up to a 2576 px long edge and 4,784 tokens per image; older models stop at 1568 px and 1,568 tokens. Examples on current models:
- 200x200 px: 64 tokens
- 1000x1000 px: 1,296 tokens
- 1920x1080 px: 2,691 tokens
- 3840x2160 px: downscaled to 2576x1449, 4,784 tokens
The same image can cost up to roughly 3x more tokens than on a standard-tier model. If you don’t need the detail, downsample before sending. For an exact count, use the Token Counting API - a separate endpoint that counts tokens without generating a response.
TypeScript example: analyzing a screenshot
import Anthropic from "@anthropic-ai/sdk";
import { readFileSync } from "fs";
const client = new Anthropic();
const imageData = readFileSync("screenshot.png").toString("base64");
const message = await client.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 4096,
messages: [
{
role: "user",
content: [
{
type: "image",
source: {
type: "base64",
media_type: "image/png",
data: imageData
}
},
{
type: "text",
text: "What's shown in the screenshot? List all UI elements."
}
]
}
]
});
for (const block of message.content) {
if (block.type === "text") {
console.log(block.text);
}
}
Streaming
Server-Sent Events
With stream: true, the response arrives as a stream of SSE events. Main event types:
| Event | Description |
|---|---|
message_start | Start of the message, carries metadata |
content_block_start | Start of a content block (text, tool_use, thinking) |
content_block_delta | Content delta: text_delta, thinking_delta, signature_delta, input_json_delta |
content_block_stop | End of a content block |
message_delta | Final data: stop_reason, usage |
message_stop | End of the message |
On current models the stream usually opens with a thinking block, so don’t treat the first content_block_start as text - branch on the block type.
Python SDK - stream helper
import anthropic
client = anthropic.Anthropic()
with client.messages.stream(
model="claude-sonnet-5-5",
max_tokens=4096,
messages=[{"role": "user", "content": "Write an async HTTP client in Python."}]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
# After completion — access the full message
final_message = stream.get_final_message()
print(f"\nTokens: {final_message.usage.input_tokens} in / {final_message.usage.output_tokens} out")
stream.text_stream yields text only. For low-level access to events, iterate over the stream object itself.
TypeScript SDK - stream
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const stream = client.messages.stream({
model: "claude-sonnet-5-5",
max_tokens: 4096,
messages: [{ role: "user", content: "Write an async HTTP client in TypeScript." }]
});
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
process.stdout.write(event.delta.text);
}
}
const finalMessage = await stream.finalMessage();
console.log(`\nTokens: ${finalMessage.usage.input_tokens} in / ${finalMessage.usage.output_tokens} out`);
Streaming with tool use
With streaming, tool parameters arrive as input_json_delta - fragments of JSON:
with client.messages.stream(
model="claude-sonnet-5-5",
max_tokens=4096,
tools=tools,
messages=[{"role": "user", "content": "What's the weather in Moscow?"}]
) as stream:
for event in stream:
if event.type == "content_block_start" and event.content_block.type == "tool_use":
print(f"Tool call: {event.content_block.name}")
elif event.type == "content_block_delta":
if event.delta.type == "input_json_delta":
print(f" parameters: {event.delta.partial_json}", end="")
elif event.delta.type == "text_delta":
print(event.delta.text, end="", flush=True)
Accumulate partial_json until content_block_stop, then parse the full JSON.
Batches API
The Batches API is asynchronous batch processing at a 50% discount. Requests don’t hold an HTTP connection open: you submit a batch and pick up the results when they’re ready. A batch holds up to 100,000 requests or 256 MB; most batches finish within an hour, and results stay available for 29 days.
When to use it
- Bulk processing: classifying thousands of documents
- Eval pipelines: running a benchmark suite
- Content generation: batch-generating descriptions
- Any task where latency doesn’t matter
Creating a batch request
Python:
import anthropic
client = anthropic.Anthropic()
batch = client.messages.batches.create(
requests=[
{
"custom_id": "request-1",
"params": {
"model": "claude-sonnet-5-5",
"max_tokens": 4096,
"messages": [
{"role": "user", "content": "Classify this ticket: 'Card payment isn't working'. Categories: billing, technical, feature_request."}
]
}
},
{
"custom_id": "request-2",
"params": {
"model": "claude-sonnet-5-5",
"max_tokens": 4096,
"messages": [
{"role": "user", "content": "Classify this ticket: 'I want a PDF export'. Categories: billing, technical, feature_request."}
]
}
}
]
)
print(f"Batch ID: {batch.id}")
print(f"Status: {batch.processing_status}")
Retrieving results
# Check the status
batch = client.messages.batches.retrieve(batch.id)
if batch.processing_status == "ended":
# Read the results
for entry in client.messages.batches.results(batch.id):
if entry.result.type == "succeeded":
text = "".join(b.text for b in entry.result.message.content if b.type == "text")
print(f"{entry.custom_id}: {text}")
else:
print(f"{entry.custom_id}: error — {entry.result.type}")
TypeScript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const batch = await client.messages.batches.create({
requests: [
{
custom_id: "request-1",
params: {
model: "claude-sonnet-5-5",
max_tokens: 4096,
messages: [
{ role: "user", content: "Summarize: 'Claude API supports streaming, tool use, and prompt caching.'" }
]
}
}
]
});
console.log(`Batch ID: ${batch.id}`);
// Later — retrieve results
const results = await client.messages.batches.results(batch.id);
for await (const entry of results) {
if (entry.result.type === "succeeded") {
const text = entry.result.message.content
.filter((b): b is Anthropic.TextBlock => b.type === "text")
.map((b) => b.text)
.join("");
console.log(`${entry.custom_id}: ${text}`);
}
}
Extended output in Batches
The Batches API supports extended output: up to 300k tokens for Opus 5.5, Sonnet 5.5, Haiku 5.5, Opus 5, Sonnet 5, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6 with the beta header output-300k-2026-03-24 (via client.beta.messages.batches.create). Useful for generating long documents, books, and reports. A single 300k-token generation can take over an hour, so keep the 24-hour batch processing window in mind.
Pricing and Optimization
For choosing between Opus and Sonnet by task and budget, see the separate comparison.
Price table
| Model | Input | Output | Cache write (5m) | Cache read | Cache write (1h) |
|---|---|---|---|---|---|
| Fable 5.1 | $10 | $50 | $12.50 (1.25x) | $0.25 (0.025x) | $20 (2x) |
| Opus 5.5 | $4 | $20 | $5 (1.25x) | $0.20 (0.05x) | $8 (2x) |
| Sonnet 5.5 | $2 | $10 | $2.50 (1.25x) | $0.10 (0.05x) | $4 (2x) |
| Haiku 5.5 (prompt up to 100K tokens) | $0.10 | $0.50 | $0.125 (1.25x) | $0.01 (0.1x) | $0.20 (2x) |
| Haiku 5.5 (prompt over 100K tokens) | $0.50 | $2.50 | $0.625 (1.25x) | $0.05 (0.1x) | $1 (2x) |
| Opus 5 (legacy) | $5 | $25 | $6.25 (1.25x) | $0.50 (0.1x) | $10 (2x) |
| Sonnet 5 (legacy) | $2 | $10 | $2.50 (1.25x) | $0.20 (0.1x) | $4 (2x) |
| Haiku 4.5 (legacy) | $1 | $5 | $1.25 (1.25x) | $0.10 (0.1x) | $2 (2x) |
The Batches API gives a 50% discount across every model.
For Haiku 5.5, the prompt length counts all input tokens, cache reads and writes included; a request over the threshold pays the higher prices for everything, output too. The other current models charge the same per-token rate across the full 1M context.
Fast mode on Opus 5.5 (speed: "fast", Claude API) costs $8 / $40 and isn’t available with the Batches API.
All prices are per million tokens (MTok). Source: Anthropic pricing.
Optimization strategies
1. Pick the model that matches the task
Don’t run Opus 5.5 on classification - Haiku 5.5 handles it at 1/40 of the price (on prompts up to 100K tokens). Sonnet 5.5 covers everyday workloads at half the Opus price. Reach for Opus 5.5 when a mistake is expensive, and for Fable 5.1 only when Opus 5.5 at higher effort still falls short.
2. Prompt caching for repeated context
If the system prompt and context documents stay the same, cache them. On Sonnet 5.5 a cache read costs 5% of the base price. At 100k tokens of cached context:
- Without cache: 100k × $2/MTok = $0.20 per request (Sonnet 5.5)
- With cache: 100k × $0.10/MTok = $0.01 per request
- Savings: 95% on input tokens
3. Batches for non-urgent work
Eval pipelines, bulk classification, dataset generation - latency doesn’t matter. The Batches API gives a 50% discount.
4. Streaming for UX
Streaming doesn’t save on tokens, but the first character appears within seconds - the user isn’t left waiting. A must for chat interfaces.
5. Control max_tokens and effort
max_tokens is a hard ceiling for thinking plus answer: set it too low and the response gets cut off. To spend fewer tokens, lower effort - it scales the whole response down, thinking included. Re-run your effort sweep when you switch models: Opus 5.5 defaults to medium, Sonnet 5.5 to high.
6. Monitor usage
Every response contains usage:
{
"input_tokens": 2095,
"output_tokens": 503,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 2000
}
Log it. Compute cost with the formula:
cost = (input_tokens * input_price + output_tokens * output_price
+ cache_creation * write_price + cache_read * read_price) / 1_000_000
Structured Outputs
Native JSON mode is available through output_config.format - GA, no beta headers needed, supported on every current model. The API guarantees a valid JSON output matching your schema.
import anthropic
import json
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"role": {"type": "string"}
},
"required": ["name", "age", "role"],
"additionalProperties": False
}
}
},
messages=[
{"role": "user", "content": "Ivan Petrov, 35, senior developer"}
]
)
text = next(b.text for b in response.content if b.type == "text")
data = json.loads(text)
# {"name": "Ivan Petrov", "age": 35, "role": "senior developer"}
In Python, client.messages.parse() with a Pydantic model is even simpler - the SDK builds the schema, validates the response, and returns the object in parsed_output (example in the JSON mode section below).
When to use which:
output_config.format- simpler, doesn’t require a tooling context, fits most cases- Strict tool use (
strict: trueon the tool) - when you’re already using tools in the same request and need schema-valid arguments. Forcedtool_choicereturns a 400 on Opus 5.5, Sonnet 5.5, and Fable 5.1, so on those models the tool is called viaautoplus an instruction in the prompt
MCP Connector
The MCP Connector lets you connect remote MCP servers directly from the Messages API - no separate MCP client needed. Useful for agentic scenarios: the model calls tools from your MCP services itself.
Requires the beta header mcp-client-2025-11-20. On the Claude API there’s also the newer mcp-client-2026-09-15: it includes everything the previous one does and adds pinning of the server’s tool list.
import anthropic
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
mcp_servers=[
{
"type": "url",
"url": "https://your-mcp-server.example.com/mcp",
"name": "my-tools",
"authorization_token": "YOUR_TOKEN" # optional
}
],
tools=[
{
"type": "mcp_toolset",
"mcp_server_name": "my-tools" # server name from mcp_servers
}
],
messages=[
{"role": "user", "content": "Complete the task using the available tools."}
],
betas=["mcp-client-2025-11-20"]
)
Tool configuration lives in the tools array as an mcp_toolset object. You can restrict the tool set via an allowlist/denylist:
tools=[
{
"type": "mcp_toolset",
"mcp_server_name": "my-tools",
"default_config": {"enabled": False}, # allowlist: disable everything by default
"configs": {
"tool1": {"enabled": True}, # enable only what's needed
"tool2": {"enabled": True}
}
}
]
Important limitations:
- Only MCP tool calls are supported - prompts and resources are not
- The server must be publicly reachable over HTTP (Streamable HTTP or SSE); local STDIO servers can’t be connected directly
- Available in beta on the Claude API, Claude Platform on AWS, Claude Models on Google Cloud (Anthropic managed), and Microsoft Foundry; not supported on Amazon Bedrock
- Tool results come from an external system - apply the same trust measures you’d apply to user input (risk of indirect prompt injection)
Best Practices
Error handling and retries
Standard HTTP codes. Main errors:
| Code | Cause | Action |
|---|---|---|
| 400 | Invalid request | Check the parameters |
| 401 | Invalid API key | Check ANTHROPIC_API_KEY |
| 429 | Rate limit | Retry with exponential backoff |
| 529 | API overloaded | Retry after 30-60 seconds |
| 500 | Server error | Retry with backoff |
The Python SDK retries automatically:
# The SDK automatically retries 429 and 5xx
client = anthropic.Anthropic(
max_retries=3, # default: 2
timeout=60.0 # seconds
)
For custom logic:
import time
from anthropic import RateLimitError, APIStatusError
def call_with_retry(client, max_retries=3, **kwargs):
for attempt in range(max_retries):
try:
return client.messages.create(**kwargs)
except RateLimitError:
wait = 2 ** attempt # 1s, 2s, 4s
print(f"Rate limited, waiting {wait}s...")
time.sleep(wait)
except APIStatusError as e:
if e.status_code >= 500:
time.sleep(2 ** attempt)
continue
raise
raise Exception("All retries exhausted")
API key security
- Store keys in environment variables, not in code
- Use separate keys for dev/staging/production
- Rotate keys regularly
- One workspace per project (isolates billing and limits)
- In CI/CD - use secrets (GitHub Secrets, Vault)
# Correct
client = anthropic.Anthropic() # picks up ANTHROPIC_API_KEY
# Wrong
client = anthropic.Anthropic(api_key="your-api-key") # key hardcoded
Structured output (JSON mode)
The recommended approach is output_config.format (covered in the dedicated section above). The old prefill trick (ending messages with an assistant turn {) no longer works: Claude 4.6 and later models reject it with a 400. Two other approaches:
Approach 1: client.messages.parse() with a Pydantic model
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
role: str
response = client.messages.parse(
model="claude-sonnet-5-5",
max_tokens=4096,
messages=[
{"role": "user", "content": "Extract data from this text: 'Ivan Petrov, 35, senior developer'."}
],
output_format=Person,
)
person = response.parsed_output # Person(name='Ivan Petrov', age=35, role='senior developer')
The SDK turns the model into a JSON Schema, sends it as output_config.format, and validates the response.
Approach 2: Strict tool use
tools = [{
"name": "extract_person",
"description": "Extract data about a person.",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"role": {"type": "string"}
},
"required": ["name", "age", "role"],
"additionalProperties": False
}
}]
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
tools=tools,
messages=[
{"role": "user", "content": "Call extract_person for this text: Ivan Petrov, 35, senior developer"}
]
)
# With strict: true, tool_use.input always matches the schema
tool_block = next((b for b in message.content if b.type == "tool_use"), None)
if tool_block:
person = tool_block.input # {"name": "Ivan Petrov", "age": 35, "role": "senior developer"}
strict: true guarantees schema-valid arguments, but with tool_choice: auto the model decides whether to call the tool, so name it in the prompt and check that the block is there. On Haiku 5.5 and the legacy models you can still force the call with tool_choice: {"type": "tool", "name": "extract_person"}.
Rate limits
Limits depend on the model and the organization’s usage tier. Main metrics:
- Requests per minute (RPM) - number of requests per minute
- Input tokens per minute (ITPM) - only uncached input counts:
cache_read_input_tokensdon’t count toward it - Output tokens per minute (OTPM) - generated tokens;
max_tokensdoesn’t affect it
Response headers carry the current limits:
anthropic-ratelimit-requests-limit: 1000
anthropic-ratelimit-requests-remaining: 999
anthropic-ratelimit-requests-reset: 2026-04-09T12:00:00Z
anthropic-ratelimit-tokens-limit: 80000
anthropic-ratelimit-tokens-remaining: 79500
Strategies:
- Exponential backoff on 429 - the SDK does this automatically; if the response has a
retry-afterheader, wait that many seconds - Batch processing via the Batches API - it has its own limits and doesn’t affect Messages API rate limits
- Multiple workspaces to isolate limits between projects
- Cache reads don’t count against ITPM - another reason to cache
Logging and monitoring
Every response contains an id (msg_...) and usage. Log both:
import logging
logger = logging.getLogger("claude_api")
response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=4096,
messages=[{"role": "user", "content": "Hello"}]
)
logger.info(
"API call",
extra={
"message_id": response.id,
"model": response.model,
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens,
"stop_reason": response.stop_reason,
}
)
In production, wire up observability: Langfuse, LangSmith, or your own pipeline. Track latency, cost per request, and distribution across models.
Wrapping Up
Claude API is one endpoint, four current models, a set of powerful features.
To get started: install the SDK, get a key, send your first request. Anthropic suggests starting with Opus 5.5; Sonnet 5.5 is half the price and faster.
For production: prompt caching on repeated context, streaming for UX, Haiku 5.5 for bulk operations, Batches for eval pipelines. Read response blocks by type and drop temperature, prefill, and forced tool_choice from older code.
For hard tasks: adaptive thinking with output_config.effort for reasoning, tool use for integrating with external systems, MCP Connector for connecting to remote MCP servers directly from the API, Opus 5.5 when accuracy is critical, Fable 5.1 when even that falls short.
Structured outputs: output_config.format - native JSON mode with no beta headers.
Full documentation - platform.claude.com/docs. SDKs: anthropic-sdk-python, anthropic-sdk-typescript. Examples and recipes - anthropic-cookbook.