Claude API for Developers
Building your own product on Claude instead of talking to it through a UI.
Intermediate
api.anthropic.com. Every request needs
an API key (from the Console, under Account Settings), sent as the x-api-key
header, plus an anthropic-version header pinning the API version you're
coding against. The official SDKs (Python, TypeScript, and others) set both automatically
once the key is in the environment.
user/assistant)
plus a model name and a token budget. A system parameter sets standing
instructions that apply to the whole conversation, separate from the message list.
from anthropic import Anthropic
client = Anthropic()
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system="You are a concise code reviewer.",
messages=[
{"role": "user", "content": "Review this function for bugs: ..."}
],
)
print(response.content[0].text)
content is a list of blocks, not a single string — a plain
text reply is one text block, but a response can carry several blocks (text
interleaved with tool calls, for instance), so production code should iterate over
response.content rather than assume content[0] is always the
whole answer.
assistant turn.
messages = [{"role": "user", "content": "What's a good name for a red panda?"}]
first = client.messages.create(model="claude-sonnet-5", max_tokens=256, messages=messages)
messages.append({"role": "assistant", "content": first.content})
messages.append({"role": "user", "content": "Now suggest one for its sibling."})
second = client.messages.create(model="claude-sonnet-5", max_tokens=256, messages=messages)
input_schema for
its arguments), pass it in the request, and Claude can respond with a tool_use
block instead of (or alongside) plain text. Your code runs the actual function and sends
the result back in the next request as a tool_result block — Claude
never executes anything itself over the API, it only asks. This is the exact mechanism
MCP and Claude Code's own tools are built on.
tools = [{
"name": "get_weather",
"description": "Get the current weather for a given location.",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City and state, e.g. San Francisco, CA"}
},
"required": ["location"],
},
}]
messages = [{"role": "user", "content": "What's the weather in San Francisco?"}]
# Claude replies with a tool_use block naming the tool and its arguments.
response = client.messages.create(
model="claude-sonnet-5", max_tokens=1024, tools=tools, messages=messages,
)
tool_use = next(b for b in response.content if b.type == "tool_use")
print(f"Claude wants to call {tool_use.name} with {tool_use.input}")
# Run the tool yourself, then send the result back as a tool_result block.
weather = "15 degrees Celsius, partly cloudy" # your actual lookup goes here
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": tool_use.id, "content": weather}
]})
followup = client.messages.create(
model="claude-sonnet-5", max_tokens=1024, tools=tools, messages=messages,
)
print(next(b for b in followup.content if b.type == "text").text)
tool_choice: auto; set tool_choice explicitly to force (or
forbid) a call rather than relying on wording alone. Tool definitions and the
tool_use/tool_result blocks they generate all count as input
tokens — a large toolset adds real cost to every request, not just the ones that end up
using it.
image content block sits alongside text in the same message — base64-encoded
inline, by URL, or by referencing a file already uploaded through the Files API (cheaper
for an image reused across many requests, since it isn't re-sent as base64 every time).
message = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_b64}},
{"type": "text", "text": "Describe this image."},
],
}],
)
with client.messages.stream(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Write a haiku about latency."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system=[{
"type": "text",
"text": huge_system_prompt,
"cache_control": {"type": "ephemeral"},
}],
messages=messages,
)
print(response.usage)
# cache_creation_input_tokens on the first call, cache_read_input_tokens on later ones
| Gotcha | What actually happens |
|---|---|
| Minimum cacheable length | most current models need roughly 1,024+ tokens before the breakpoint to cache at all; shorter prompts are silently sent uncached, no error |
| Breakpoint placement | only the exact marked block is cached — put it on the last static content, never on something that changes per request (like a timestamp) |
| Cache invalidation | changing tool definitions invalidates everything cached; changing tool_choice or adding/removing images only invalidates the messages, not the system prompt |
| Default TTL | 5 minutes, refreshed automatically on each hit; a 1-hour TTL is available at 2× the write cost for content reused less frequently |
429 with a retry-after header telling you
exactly how long to back off.
cache_read_input_tokens doesn't. Heavy prompt caching
doesn't just cut cost, it effectively raises your real throughput ceiling too.