Project 05 — Your First Messages API Call
Enforces: the Messages API surface — auth, a system+user message, reading content blocks, a 2-turn conversation, and streaming (from building-with-the-claude-api) Surface: code (Python or TypeScript) · Time: ~60 min · Difficulty: 🟡 core
Why this project
Every later project — tools, extraction, caching, RAG, evals — is a variation on one call: messages.create. If that call and its response shape are muscle memory, everything downstream is composition. This project gets you from an empty file to a streaming, multi-turn conversation.
What you'll build
A small script that authenticates, sends a system+user message, prints the response from its content blocks, holds a two-turn conversation, and re-runs the same prompt with streaming.
Steps
- Set up auth — install the SDK (
pip install anthropicornpm install @anthropic-ai/sdk) and put your key in the environment asANTHROPIC_API_KEY. Never hardcode the key. The default client reads it for you:import anthropic client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY - Send a system + user message — the system prompt sets behavior; the user message is the turn. Default to
claude-opus-4-8:resp = client.messages.create( model="claude-opus-4-8", max_tokens=1024, system="You are a concise assistant. Answer in one sentence.", messages=[{"role": "user", "content": "What is the Messages API?"}], ) - Read the response from content blocks —
resp.contentis a list of blocks, not a string. Check.typebefore reading.text:for block in resp.content: if block.type == "text": print(block.text) print(resp.stop_reason, resp.usage.input_tokens, resp.usage.output_tokens) - Hold a two-turn conversation — the API is stateless: you resend the whole history each turn. Append the assistant's reply, then your follow-up:
messages = [{"role": "user", "content": "My name is Alex."}] r1 = client.messages.create(model="claude-opus-4-8", max_tokens=256, messages=messages) messages.append({"role": "assistant", "content": r1.content}) messages.append({"role": "user", "content": "What's my name?"}) r2 = client.messages.create(model="claude-opus-4-8", max_tokens=256, messages=messages) # r2 should recall "Alex" - Turn on streaming — use the
.stream()helper and print tokens as they arrive; grab the final message at the end:with client.messages.stream( model="claude-opus-4-8", max_tokens=1024, messages=[{"role": "user", "content": "Explain streaming in two sentences."}], ) as stream: for text in stream.text_stream: print(text, end="", flush=True) final = stream.get_final_message() print("\n", final.usage.output_tokens) - Note the cloud variants — the same
messages.createsurface runs on Amazon Bedrock (use theAnthropicBedrockclient; model IDs take ananthropic.prefix, e.g.anthropic.claude-opus-4-8; region required) and Google Vertex AI (useAnthropicVertexwithproject_id+region; bare model IDs, no prefix; auth via GCP ADC). Same calls, different client constructor. Write a one-line comment in your script noting which client each would use.
Acceptance criteria — you're done when
- Your key is read from the environment — it does not appear in the source.
- You sent a system + user message and printed the reply by iterating
contentblocks (guarding on.type). - You printed
stop_reasonand token usage fromresp.usage. - Your two-turn conversation resends history and the model recalls turn-1 context.
- You ran the prompt with
.stream()and printed tokens incrementally, then read the final message. - Your script has a comment naming the Bedrock and Vertex client/model-ID differences.
- You journaled one thing that surprised you about the response shape in learning-journal-template.
Stretch goals
- Add typed error handling — catch
anthropic.RateLimitError,anthropic.APIStatusError,anthropic.APIConnectionErrorseparately (most-specific first). - Count tokens before sending with
client.messages.count_tokens(...)and print the estimate. - Set
max_tokens=64000with streaming and observe why large outputs must stream (HTTP timeouts).
Self-assessment rubric
| Level | Signal |
|---|---|
| 🟢 Got it | You can write a streaming, multi-turn call from memory and explain the content-block shape. |
| 🟡 Almost | The call works but you still reach for resp.content[0].text without guarding on type. |
| 🔴 Revisit | Auth or the response shape tripped you up; re-watch building-with-the-claude-api. |
See also
- Course: building-with-the-claude-api
- Next project: p06-tool-use-weather-agent
- Deeper: claude-with-amazon-bedrock, claude-with-vertex-ai