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.
- Go to the Anthropic Console.1
- Sign up and buy API credits through the Console’s billing page.
- Create a new API Key and name it
nanocode. - Copy the key (it starts with
sk-ant-...).
![]() |
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.envfile and loads variables intoos.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 theNoneheader value, so the API would receive a request without its API-key header and reject authentication. - Line 21: The
anthropic-versionheader selects the API contract. Keep2023-06-01; it is not the model’s release date. - Line 27:
claude-sonnet-5-5specifies which model we want. - Line 28:
max_tokenscaps this response at 4,096 output tokens. It does not cap spending across repeated calls. - Lines 29-30:
between_toolswith 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.
![]() |
Aside: This |
![]() |
Aside: Check the Anthropic Console’s Usage tab to monitor spending. The response’s |
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.

