Chapter 2: The Raw Request

Most tutorials will tell you to pip install anthropic. We are not going to do that.

SDKs hide the truth. They add layers of abstraction that make “Hello World” easy but make debugging “Error 400” a nightmare. When you learn the SDK, you learn the SDK. When you learn the raw HTTP call, you learn the protocol underneath every SDK.

We are going to send a message to Claude using nothing but the requests library. Claude is Anthropic’s model family; we use Sonnet 5.5 here.

Get an API Key

To talk to Claude, you need an API Key. This is a long string of characters that acts like a password tied to your billing account.

  1. Go to the Anthropic Console.1
  2. Sign up and buy API credits through the Console’s billing page.
  3. Create a new API Key and name it nanocode.
  4. Copy the key (it starts with sk-ant-...).
An icon indicating this blurb contains a warning

Warning: Treat this key like a password. Anyone with it can spend your money.

The Vault (.env)

We need a safe place to store this key. We never put keys directly in code.

Create a file named .env in your project root:

1 touch .env

Open it and paste your key:

1 ANTHROPIC_API_KEY=sk-ant-api03-...

We installed python-dotenv in Chapter 1 for exactly this purpose—it reads .env and loads the values into os.environ.

The Anatomy of a Request

To talk to an LLM, we send an HTTP POST request to:

https://api.anthropic.com/v1/messages

This request needs three things: authentication in the headers (your API key), configuration in the body (which model, how many tokens), and the message itself.

The Headers

Anthropic requires three headers:

Header Value Purpose
x-api-key Your secret key Authentication
anthropic-version 2023-06-01 API version
content-type application/json Format

The Payload

The “Messages API” expects a list of message dictionaries:

1 "messages": [
2     {"role": "user", "content": "Hello, world!"}
3 ]

Each message has a role (either "user" or "assistant") and content (the text).

The Code

Create a file called test_api.py. This is a “smoke test” to prove our connection works. We will delete it later.

The Context: We are writing linear, procedural code. No functions, no classes. We want to see the bare metal.

The Code:

 1 import os
 2 import requests
 3 import json
 4 from dotenv import load_dotenv
 5 
 6 # 1. Load the vault
 7 load_dotenv()
 8 api_key = os.getenv("ANTHROPIC_API_KEY")
 9 
10 # Fail early with a clear message instead of an API authentication error
11 if not api_key:
12     print("Error: ANTHROPIC_API_KEY not found in .env")
13     exit(1)
14 
15 # 2. Define the target
16 url = "https://api.anthropic.com/v1/messages"
17 
18 # 3. Authenticate
19 headers = {
20     "x-api-key": api_key,
21     "anthropic-version": "2023-06-01",
22     "content-type": "application/json"
23 }
24 
25 # 4. Construct the payload
26 payload = {
27     "model": "claude-sonnet-5-5",
28     "max_tokens": 4096,
29     "thinking": {"type": "between_tools"},
30     "output_config": {"effort": "medium"},
31     "messages": [
32         {"role": "user", "content": "Hello, are you ready to code?"}
33     ]
34 }
35 
36 # 5. Fire! (No safety net)
37 print("📡 Sending request to Claude...")
38 response = requests.post(url, headers=headers, json=payload, timeout=120)
39 
40 # 6. Inspect the raw result
41 print(f"Status: {response.status_code}")
42 
43 if response.status_code == 200:
44     # Success: Print the beautiful JSON
45     print("Response:")
46     print(json.dumps(response.json(), indent=2))
47 else:
48     # Failure: Print the ugly raw text so we can debug
49     print("Error:", response.text)

The Walkthrough:

  • Line 7: load_dotenv() finds the .env file and loads variables into os.environ.
  • Line 8: We grab the API key. Never hardcode this.
  • Lines 11-13: Fail early with a clear configuration error if the key is missing. For this requests.post() call, Requests removes the None header value, so the API would receive a request without its API-key header and reject authentication.
  • Line 21: The anthropic-version header selects the API contract. Keep 2023-06-01; it is not the model’s release date.
  • Line 27: claude-sonnet-5-5 specifies which model we want.
  • Line 28: max_tokens caps this response at 4,096 output tokens. It does not cap spending across repeated calls.
  • Lines 29-30: between_tools with medium effort keeps this no-tools example focused on text. Claude uses thinking between tool calls in this mode; we introduce adaptive thinking with visible summaries when the agent gains a brain.2
  • Line 38: We fire the request with a 2-minute timeout. No try/except—if the network is down, let Python crash. You need to see where it fails.
  • Lines 43-49: Check the status code. 200 means success (pretty-print the JSON). Anything else means we print the raw error text for debugging.

Run It

1 python test_api.py

If everything works, you should see:

Status: 200
Response:
{
  "id": "msg_01...",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "Hello! Yes, I'm ready to code..."
    }
  ],
  "stop_reason": "end_turn",
  "usage": {
    "input_tokens": 15,
    "output_tokens": 81
  }
}

Troubleshooting

Error Cause Fix
401 Unauthorized Missing or invalid API key Check .env is loading with bool(os.getenv("ANTHROPIC_API_KEY")). This checks presence without printing the secret; a present key can still be invalid.
400 Bad Request Malformed JSON Did you forget max_tokens? Is messages a list?
429 Rate Limit Too many requests or no credits Wait, or add credits to your account.

Cleaning Up

We proved we can talk to the brain. Delete test_api.py—we’re done with it.

An icon indicating this blurb contains information

Aside: This test_api.py is throwaway code—a one-time smoke test. Real automated tests (with FakeBrain and pytest) come in Chapter 3. You should always delete this file after verifying your API connection works.

An icon indicating this blurb contains information

Aside: Check the Anthropic Console’s Usage tab to monitor spending. The response’s usage object reports token counts. A session’s bill depends on repeated history, cached input, output, and thinking; the number of exchanges alone cannot predict it.

Wrapping Up

That’s a raw API call: headers, JSON payload, response parsing. No abstraction between you and the wire. When something breaks (and it will), you’ll know exactly which layer failed because there’s only one layer.

One problem: Claude has total amnesia. Every request is a blank slate. We’re going to fake memory by replaying the entire conversation history on every turn.


  1. https://console.anthropic.com/settings/keys↩︎

  2. https://platform.claude.com/docs/en/models/sonnet-5-5/migration-guide↩︎

  3. https://platform.claude.com/docs/en/about-claude/pricing↩︎