Claude Academy
Sign in

Vault / course/projects/p05-first-api-call.md

updated 2026-06-25

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

  1. Set up auth — install the SDK (pip install anthropic or npm install @anthropic-ai/sdk) and put your key in the environment as ANTHROPIC_API_KEY. Never hardcode the key. The default client reads it for you:
    import anthropic
    client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY
    
  2. 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?"}],
    )
    
  3. Read the response from content blocksresp.content is a list of blocks, not a string. Check .type before 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)
    
  4. 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"
    
  5. 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)
    
  6. Note the cloud variants — the same messages.create surface runs on Amazon Bedrock (use the AnthropicBedrock client; model IDs take an anthropic. prefix, e.g. anthropic.claude-opus-4-8; region required) and Google Vertex AI (use AnthropicVertex with project_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 content blocks (guarding on .type).
  • You printed stop_reason and token usage from resp.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.APIConnectionError separately (most-specific first).
  • Count tokens before sending with client.messages.count_tokens(...) and print the estimate.
  • Set max_tokens=64000 with streaming and observe why large outputs must stream (HTTP timeouts).

Self-assessment rubric

LevelSignal
🟢 Got itYou can write a streaming, multi-turn call from memory and explain the content-block shape.
🟡 AlmostThe call works but you still reach for resp.content[0].text without guarding on type.
🔴 RevisitAuth or the response shape tripped you up; re-watch building-with-the-claude-api.

See also