# Claude API: The Complete Developer Guide

> A comprehensive Claude API tutorial: Messages API, tool use, adaptive thinking, prompt caching, vision, streaming, batches, MCP connector, structured outputs. Examples in Python and TypeScript.
> Author: Roman Belov · Published: 2026-06-28 · Source: https://futurecraft.pro/blog/claude-api-developer-guide/

Claude API is the programmatic interface for working with Anthropic's models. One endpoint — the Messages API — covers text, images, tool use, streaming, and extended 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.

## 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

Three current-generation models:

| Model | API ID | Context | Max output | Price (input/output per MTok) |
|--------|--------|----------|------------|----------------------------|
| **Opus 4.8** | `claude-opus-4-8` | 1M | 128k | $5 / $25 |
| **Sonnet 4.6** | `claude-sonnet-4-6` | 1M | 128k | $3 / $15 |
| **Haiku 4.5** | `claude-haiku-4-5` | 200k | 64k | $1 / $5 |

**Opus 4.8** — the flagship. Maximum accuracy on code, reasoning, and agentic tasks. 1M tokens of context, 128k output. Pick it when a mistake is expensive.

**Sonnet 4.6** — the workhorse. Fast, smart, 1.67x cheaper than Opus on output ($15 vs $25). Covers 90% of production workloads: chatbots, content generation, document analysis.

**Haiku 4.5** — speed and economy. Classification, data extraction, moderation, bulk processing. 3x cheaper than Sonnet on output ($5 vs $15).

### 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
- **Extended thinking** — access to the reasoning trace
- **Prompt caching** — caching of repeated context
- **Batches** — asynchronous batch processing at a 50% discount
- **Full control** — temperature, stop sequences, metadata

## Quick Start

### Getting an API key

1. Sign up at [console.anthropic.com](https://console.anthropic.com)
2. Go to **Settings → API Keys**
3. Create a key (starts with `sk-ant-`)
4. Top up your balance under **Billing** (minimum $5)

The key is tied to a workspace. One workspace — one set of keys, limits, and billing.

### Installing the SDK

**Python:**

```bash
pip install anthropic
```

**TypeScript:**

```bash
npm install @anthropic-ai/sdk
```

The SDK picks up the key from the `ANTHROPIC_API_KEY` environment variable:

```bash
export ANTHROPIC_API_KEY="sk-ant-api03-..."
```

### The first request

**Python:**

```python
import anthropic

client = anthropic.Anthropic()

message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Explain the difference between REST and GraphQL in three sentences."}
    ]
)

print(message.content[0].text)
```

**TypeScript:**

```typescript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const message = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  messages: [
    { role: "user", content: "Explain the difference between REST and GraphQL in three sentences." }
  ]
});

console.log(message.content[0].text);
```

The response lives in `message.content` — an array of content blocks. For plain text, it's a single block with `type: "text"`.

### Streaming — real-time output

Streaming delivers tokens as they're generated — the user sees output immediately.

**Python:**

```python
import anthropic

client = anthropic.Anthropic()

with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    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:**

```typescript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const stream = await client.messages.stream({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  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-4-8`, `claude-sonnet-4-6`, `claude-haiku-4-5` |
| `messages` | array | Yes | Array of messages with `user` and `assistant` roles |
| `max_tokens` | integer | Yes | Max tokens in the response (Opus 4.8 / Sonnet 4.6: up to 128k, Haiku 4.5: up to 64k) |
| `system` | string/array | No | System prompt — instructions for the model |
| `temperature` | float | No | Randomness: 0.0–1.0 (default: 1.0). Lower is more deterministic |
| `top_p` | float | No | Nucleus sampling: 0.0–1.0. Alternative to temperature |
| `top_k` | integer | No | Sampling from the top-K tokens. For advanced cases |
| `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:**

```python
message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    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:

```python
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:**

```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-4-6",
    max_tokens=1024,
    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.

### Stop sequences

Custom strings that stop generation when produced:

```python
message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    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:

```python
message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    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:

```python
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:

1. You send a request with `tools`
2. Claude returns a `tool_use` block with the tool name and parameters
3. You execute the call
4. You send the result back as a `tool_result`
5. Claude produces the final answer (or calls the next tool)

### Full example: a weather bot

**Python:**

```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-4-6",
    max_tokens=1024,
    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
    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-4-6",
        max_tokens=1024,
        tools=tools,
        messages=messages
    )

    print(final_response.content[0].text)
```

**TypeScript:**

```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-4-6",
  max_tokens: 1024,
  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-4-6",
    max_tokens: 1024,
    tools,
    messages
  });

  console.log((finalResponse.content[0] as Anthropic.TextBlock).text);
}
```

### 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 |
| `{"type": "tool", "name": "get_weather"}` | The model must call a specific tool |
| `{"type": "none"}` | Tools are disabled |

```python
# Forcing a specific tool call
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "get_weather"},
    messages=[{"role": "user", "content": "Tell me about Moscow."}]
)
```

Forced tool use (`type: "tool"`) is handy for structured output: describe a "tool" with the JSON structure you need — Claude fills it in.

### 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:

```python
# 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

Extended thinking shows how Claude reasons before answering. The model breaks the problem into steps, checks hypotheses, catches mistakes — and only then answers.

### 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"

### Parameters

With the release of Claude 4.6, Anthropic moved to **adaptive thinking** — the model decides for itself how much to think. The `budget_tokens` parameter is **deprecated** as of Opus 4.6 and Sonnet 4.6; on Opus 4.7, Opus 4.8, and newer, passing it returns a `400 Bad Request`.

The new syntax is `thinking: {type: "adaptive"}`, with depth controlled via `output_config.effort`:

```python
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=16000,
    thinking={"type": "adaptive"},
    output_config={"effort": "high"},  # low | medium | high (default) | 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`. At `low`/`medium`, Claude may skip thinking on simple tasks; at `max`, it always thinks deeply.

### Reading thinking blocks

The response contains `thinking` and `text` blocks:

```python
for block in response.content:
    if block.type == "thinking":
        print(f"Reasoning: {block.thinking}")
    elif block.type == "text":
        print(f"Answer: {block.text}")
```

On Opus 4.7+ and Opus 4.8, thinking is hidden by default (`display: "omitted"`) — the block comes back with an empty `thinking` field. On earlier Claude 4 models, the default is `display: "summarized"` (a short summary of the reasoning). Control display explicitly: `thinking: {type: "adaptive", display: "summarized"}` returns the gist of the reasoning, `display: "omitted"` hides it and reduces latency when streaming. Billing is for the full thinking tokens regardless of the `display` value.

### Streaming with extended thinking

**Python:**

```python
with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=16000,
    thinking={"type": "adaptive"},
    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)
```

### Limitations and compatibility

- On Opus 4.7, Opus 4.8, and newer, `temperature`, `top_p`, and `top_k` are not accepted at non-default values (400)
- No prefill (starting the response with an assistant message)
- For heavy tasks at `effort: max`, `max_tokens` of at least 64k is recommended
- For heavy tasks, the Batches API is recommended

### Cost

Thinking tokens are billed as output tokens. The cost depends on the amount actually spent (not on `effort` — that's just a hint). Watch `usage.output_tokens_details.thinking_tokens`, not just `usage.output_tokens`.

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 price — a 10x savings on repeated context.

### 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.

### TTL: two options

| TTL | Cache write | Cache read | When to use |
|-----|------------|------------|-------------------|
| 5 minutes (default) | 1.25x base price | 0.1x base price | Frequent requests (more often than every 5 minutes) |
| 1 hour | 2x base price | 0.1x base price | Agentic tasks, long sessions |

### Minimum size for caching

| Model | Minimum tokens |
|--------|----------------|
| Opus 4.8 | 1024 |
| 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:**

```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-4-6",
    max_tokens=1024,
    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

```python
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 automatic caching by default — without `cache_control`, nothing gets cached. For multi-turn conversations there's a simpler option: a 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.

## Vision (Multimodality)

Claude accepts images in messages. Two ways: base64 and URL.

### Supported formats

JPEG, PNG, GIF, WebP. Maximum size — 10 MB per image via the API (5 MB on Amazon Bedrock and Google Cloud).

### Base64

**Python:**

```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-4-6",
    max_tokens=1024,
    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."
                }
            ]
        }
    ]
)

print(message.content[0].text)
```

### URL

```python
message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    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

Cost depends on size. Images are scaled automatically — Claude doesn't process pixels 1:1. Rough figures:

- Small image (up to 384x384): ~170 tokens
- Medium (up to 1024x1024): ~1600 tokens
- Large (up to 2048x2048): ~6400 tokens

For an exact count, use the Token Counting API — a separate endpoint that counts tokens without generating a response.

### TypeScript example: analyzing a screenshot

```typescript
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-4-6",
  max_tokens: 1024,
  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."
        }
      ]
    }
  ]
});

console.log(message.content[0].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`, `input_json_delta` |
| `content_block_stop` | End of a content block |
| `message_delta` | Final data: `stop_reason`, `usage` |
| `message_stop` | End of the message |

### Python SDK — stream helper

```python
import anthropic

client = anthropic.Anthropic()

with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    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, use `stream.events`.

### TypeScript SDK — stream

```typescript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const stream = await client.messages.stream({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  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:

```python
with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    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.

### 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:**

```python
import anthropic

client = anthropic.Anthropic()

batch = client.messages.batches.create(
    requests=[
        {
            "custom_id": "request-1",
            "params": {
                "model": "claude-sonnet-4-6",
                "max_tokens": 1024,
                "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-4-6",
                "max_tokens": 1024,
                "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

```python
# 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":
            print(f"{entry.custom_id}: {entry.result.message.content[0].text}")
        else:
            print(f"{entry.custom_id}: error — {entry.result.type}")
```

### TypeScript

```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-4-6",
        max_tokens: 1024,
        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") {
    console.log(`${entry.custom_id}: ${(entry.result.message.content[0] as Anthropic.TextBlock).text}`);
  }
}
```

### Extended output in Batches

The Batches API supports extended output: up to 300k tokens for Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6 with the beta header `output-300k-2026-03-24`. Useful for generating long documents, books, and reports.

## Pricing and Optimization

### Price table

| Model | Input | Output | Cache write (5m) | Cache read | Cache write (1h) |
|--------|-------|--------|------------------|------------|------------------|
| Opus 4.8 | $5 | $25 | $6.25 (1.25x) | $0.50 (0.1x) | $10 (2x) |
| Sonnet 4.6 | $3 | $15 | $3.75 (1.25x) | $0.30 (0.1x) | $6 (2x) |
| Haiku 4.5 | $1 | $5 | $1.25 (1.25x) | $0.10 (0.1x) | $2 (2x) |

The Batches API gives a 50% discount across every model.

All prices are per million tokens (MTok).

### Optimization strategies

**1. Pick the model that matches the task**

Don't run Opus 4.8 on classification — Haiku handles it at 1/25 of the cost. Sonnet covers most production workloads. Reach for Opus when a mistake is expensive.

**2. Prompt caching for repeated context**

If the system prompt and context documents stay the same, cache them. A cache read costs 10% of the base price. At 100k tokens of cached context:

- Without cache: 100k × $3/MTok = $0.30 per request (Sonnet)
- With cache: 100k × $0.30/MTok = $0.03 per request
- Savings: 90% 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**

Don't set `max_tokens: 128000` if the answer takes 200 tokens. Claude stops before the limit, but an inflated value slows down processing.

**6. Monitor usage**

Every response contains `usage`:

```json
{
  "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

As of 2026, native JSON mode is available through `output_config.format` — GA, no beta headers needed. It's an alternative to forced tool use: the API guarantees a valid JSON output matching your schema.

```python
import anthropic
import json

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    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"}
    ]
)

data = json.loads(response.content[0].text)
# {"name": "Ivan Petrov", "age": 35, "role": "senior developer"}
```

**When to use which:**

- `output_config.format` — simpler, doesn't require a tooling context, fits most cases
- Forced tool use — when you're already using tools in the same request, or need more complex routing logic

## 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`.

```python
import anthropic

client = anthropic.Anthropic()

response = client.beta.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    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:

```python
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
- Available on Claude API, Claude Platform on AWS, and Microsoft Foundry; not supported on Bedrock or Google Cloud
- 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:

```python
# The SDK automatically retries 429 and 5xx
client = anthropic.Anthropic(
    max_retries=3,  # default: 2
    timeout=60.0     # seconds
)
```

For custom logic:

```python
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)

```python
# Correct
client = anthropic.Anthropic()  # picks up ANTHROPIC_API_KEY

# Wrong
client = anthropic.Anthropic(api_key="sk-ant-api03-...")  # key hardcoded
```

### Structured output (JSON mode)

The recommended approach is `output_config.format` (covered in the [dedicated section](#structured-outputs) above). For compatibility with older code or trickier cases — two classic approaches:

**Approach 1: Prompt instruction + prefill**

```python
message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Extract data from this text: 'Ivan Petrov, 35, senior developer'. Return JSON with fields name, age, role."},
        {"role": "assistant", "content": "{"}  # prefill — Claude continues the JSON
    ]
)
# The result starts with { — parse it as JSON
result = "{" + message.content[0].text
```

**Approach 2: Forced tool use**

```python
tools = [{
    "name": "extract_person",
    "description": "Extract data about a person.",
    "input_schema": {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "age": {"type": "integer"},
            "role": {"type": "string"}
        },
        "required": ["name", "age", "role"]
    }
}]

message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "extract_person"},
    messages=[
        {"role": "user", "content": "Ivan Petrov, 35, senior developer"}
    ]
)

# Guaranteed JSON in tool_use.input
tool_block = next(b for b in message.content if b.type == "tool_use")
person = tool_block.input  # {"name": "Ivan Petrov", "age": 35, "role": "senior developer"}
```

Forced tool use is more reliable — Claude always returns valid JSON matching the schema.

### Rate limits

Limits depend on the model and plan. Main metrics:

- **Requests per minute (RPM)** — number of requests per minute
- **Tokens per minute (TPM)** — number of tokens per minute
- **Tokens per day (TPD)** — daily limit

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:

1. **Exponential backoff** on 429 — the SDK does this automatically
2. **Batch processing** via the Batches API — doesn't count against RPM/TPM limits
3. **Multiple workspaces** to isolate limits between projects
4. **Cache hits don't count** against rate limits — another reason to cache

### Logging and monitoring

Every response contains an `id` (`msg_...`) and `usage`. Log both:

```python
import logging

logger = logging.getLogger("claude_api")

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    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, three models, a set of powerful features.

**To get started**: install the SDK, get a key, send your first request. Sonnet 4.6 covers 90% of tasks.

**For production**: prompt caching on repeated context, streaming for UX, Haiku for bulk operations, Batches for eval pipelines.

**For hard tasks**: adaptive thinking (`thinking: {type: "adaptive"}` + `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 4.8 when accuracy is critical.

**Structured outputs**: `output_config.format` — native JSON mode with no beta headers, GA since 2026.

Full documentation — [docs.anthropic.com](https://docs.anthropic.com). SDKs: [anthropic-sdk-python](https://github.com/anthropics/anthropic-sdk-python), [anthropic-sdk-typescript](https://github.com/anthropics/anthropic-sdk-typescript). Examples and recipes — [anthropic-cookbook](https://github.com/anthropics/anthropic-cookbook).
