APIs & Keys
Every AI API works the same way: send a request, get a response. The details change, the pattern doesn't.
Prerequisites: Phase 0, Lesson 01
Video coming soon
An AI-narrated walkthrough for this module is on the way. Everything you need is in the lesson below.
#Learning Objectives
- Store API keys securely using environment variables and
.envfiles - Make an LLM API call using both the Anthropic Python SDK and raw HTTP
- Compare SDK-based and raw HTTP request/response formats for debugging
- Identify and handle common API errors including authentication and rate limits
#The Problem
Starting from Phase 11, you'll call LLM APIs (Anthropic, OpenAI, Google). In Phase 13-16 you'll build agents that use these APIs in loops. You need to know how API keys work, how to store them safely, and how to make your first API call.
#The Concept
sequenceDiagram
participant C as Your Code
participant S as API Server
C->>S: HTTP Request (with API key)
S->>C: HTTP Response (JSON)
Every API call has:
- An endpoint (URL)
- An API key (authentication)
- A request body (what you want)
- A response body (what you get back)
#Build It
#Step 1: Store API keys safely
Never put API keys in code. Use environment variables.
export ANTHROPIC_API_KEY="sk-ant-..."
export OPENAI_API_KEY="sk-..."Or use a .env file (add it to .gitignore):
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...#Step 2: First API call (Python)
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=256,
messages=[{"role": "user", "content": "What is a neural network in one sentence?"}]
)
print(response.content[0].text)#Step 3: First API call (TypeScript)
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 256,
messages: [{ role: "user", content: "What is a neural network in one sentence?" }],
});
console.log(response.content[0].text);#Step 4: Raw HTTP (no SDK)
import os
import urllib.request
import json
url = "https://api.anthropic.com/v1/messages"
headers = {
"Content-Type": "application/json",
"x-api-key": os.environ["ANTHROPIC_API_KEY"],
"anthropic-version": "2023-06-01",
}
body = json.dumps({
"model": "claude-sonnet-4-20250514",
"max_tokens": 256,
"messages": [{"role": "user", "content": "What is a neural network in one sentence?"}],
}).encode()
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
print(result["content"][0]["text"])This is what the SDKs do under the hood. Understanding the raw HTTP call helps when debugging.
#Use It
For this course:
| API | When you need it | Free tier |
|---|---|---|
| Anthropic (Claude) | Phases 11-16 (agents, tools) | $5 credit on signup |
| OpenAI | Phase 11 (comparison) | $5 credit on signup |
| Hugging Face | Phases 4-10 (models, datasets) | Free |
You don't need all of them right now. Set them up when the lesson requires it.
#Ship It
This lesson produces:
outputs/prompt-api-troubleshooter.md- diagnose common API errors
#Exercises
- Get an Anthropic API key and make your first API call
- Try the raw HTTP version and compare the response format to the SDK version
- Intentionally use a wrong API key and read the error message
#Key Terms
| Term | What people say | What it actually means |
|---|---|---|
| API key | "Password for the API" | A unique string that identifies your account and authorizes requests |
| Rate limit | "They're throttling me" | Maximum requests per minute/hour to prevent abuse and ensure fair usage |
| Token | "A word" (in API context) | A billing unit: input and output tokens are counted and charged separately |
| Streaming | "Real-time responses" | Getting the response word by word instead of waiting for the full response |
Run this lesson in the Code Lab
A real JupyterLab, with this lesson's code files (and a ready-to-run notebook). The Quick Lab opens instantly in your browser. The Cloud Lab adds a terminal, saved files and the full toolchain.
import os
import json
import urllib.request
def call_with_sdk():
try:
import anthropic
except ImportError:
print("Install the SDK: pip install anthropic")
return
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=256,
messages=[{"role": "user", "content": "What is a neural network in one sentence?"}]
)
print(f"SDK response: {response.content[0].text}")
print(f"Tokens used: {response.usage.input_tokens} in, {response.usage.output_tokens} out")
def call_raw_http():
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
print("Set ANTHROPIC_API_KEY environment variable first")
return
url = "https://api.anthropic.com/v1/messages"
headers = {
"Content-Type": "application/json",
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
}
body = json.dumps({
"model": "claude-sonnet-4-20250514",
"max_tokens": 256,
"messages": [{"role": "user", "content": "What is a neural network in one sentence?"}],
}).encode()
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
print(f"Raw HTTP response: {result['content'][0]['text']}")
print(f"Tokens used: {result['usage']['input_tokens']} in, {result['usage']['output_tokens']} out")
if __name__ == "__main__":
print("=== API Calls ===\n")
print("1. Using the SDK:")
call_with_sdk()
print("\n2. Using raw HTTP:")
call_raw_http()
// Phase 0 · Lesson 04 — APIs and keys (TypeScript port).
// Reads ANTHROPIC_API_KEY from env, parses a minimal .env file, then makes one
// /v1/messages call with global fetch. Set MOCK=1 to skip the network entirely.
// Refs: https://docs.anthropic.com/en/api/messages
// https://nodejs.org/api/process.html#processenv
// https://nodejs.org/api/globals.html#fetch (Node 18+ ships fetch)
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import process from "node:process";
type MessagesRequest = {
model: string;
max_tokens: number;
messages: { role: "user" | "assistant"; content: string }[];
};
type MessagesResponse = {
content: { type: string; text: string }[];
usage: { input_tokens: number; output_tokens: number };
};
// .env loader. Same shape every framework follows; we skip a dep to stay
// portable. KEY=VALUE per line, # comments, optional surrounding quotes.
function loadDotenv(path: string): Record<string, string> {
let raw: string;
try {
raw = readFileSync(path, "utf8");
} catch {
return {};
}
const out: Record<string, string> = {};
for (const line of raw.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
let value = trimmed.slice(eq + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
out[key] = value;
}
return out;
}
function mergeEnv(): NodeJS.ProcessEnv {
// process.env wins so users can override the file without editing it.
const fromFile = loadDotenv(resolve(process.cwd(), ".env"));
return { ...fromFile, ...process.env };
}
// Fixture matches the real /v1/messages response shape, so the surrounding
// code is identical whether MOCK=1 or not.
const MOCK_RESPONSE: MessagesResponse = {
content: [
{
type: "text",
text: "A neural network is a stack of differentiable functions that learns patterns by adjusting weights against a loss signal.",
},
],
usage: { input_tokens: 12, output_tokens: 28 },
};
async function callMessages(apiKey: string, request: MessagesRequest): Promise<MessagesResponse> {
if (process.env.MOCK === "1" || apiKey === "mock") {
return MOCK_RESPONSE;
}
const resp = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify(request),
});
if (!resp.ok) {
const body = await resp.text();
throw new Error(`anthropic ${resp.status}: ${body.slice(0, 200)}`);
}
return (await resp.json()) as MessagesResponse;
}
async function main(): Promise<number> {
const env = mergeEnv();
const apiKey = env.ANTHROPIC_API_KEY ?? "mock";
const usingMock = process.env.MOCK === "1" || apiKey === "mock";
process.stdout.write("=== API Calls ===\n\n");
process.stdout.write(
usingMock
? "Mode: MOCK (no network). Unset MOCK and export ANTHROPIC_API_KEY for a live call.\n\n"
: "Mode: LIVE.\n\n",
);
const request: MessagesRequest = {
model: "claude-sonnet-4-6",
max_tokens: 256,
messages: [{ role: "user", content: "What is a neural network in one sentence?" }],
};
try {
const response = await callMessages(apiKey, request);
const text = response.content[0]?.text ?? "";
process.stdout.write(`response: ${text}\n`);
process.stdout.write(
`tokens: ${response.usage.input_tokens} in, ${response.usage.output_tokens} out\n`,
);
return 0;
} catch (err) {
process.stderr.write(`request failed: ${(err as Error).message}\n`);
return 1;
}
}
main().then((code) => process.exit(code));
5 questions on this lesson
Answer each one and see straight away whether you were right and why. Your first answer to a question counts. You are not signed in, so this is practice and nothing is saved.
Reusable outputs from this lesson. Copy them into your own work.
---
name: prompt-api-troubleshooter
description: Diagnose and fix common AI API errors (auth, rate limits, timeouts)
phase: 0
lesson: 4
---
You diagnose AI API errors. When someone shares an error, identify the cause and give the fix.
Common errors and fixes:
- **401 Unauthorized**: API key is wrong or missing. Check the environment variable is set and the key is valid.
- **403 Forbidden**: API key doesn't have permission for this endpoint or model.
- **429 Too Many Requests**: Rate limited. Wait and retry, or reduce request frequency.
- **400 Bad Request**: Request body is malformed. Check required fields, model name spelling, message format.
- **500/502/503**: Server-side issue. Wait a minute and retry.
- **Timeout**: Request took too long. Reduce max_tokens or use streaming.
- **Connection refused**: Wrong base URL or network issue. Check the endpoint URL.
Diagnostic steps:
1. Is the API key set? `echo $ANTHROPIC_API_KEY | head -c 10`
2. Is the key valid? Try a minimal request.
3. Is the request format correct? Compare to the docs.
4. Is there a network issue? `curl -I https://api.anthropic.com`
The text and code of this lesson are based on AI Engineering from Scratch by Rohit Ghumare, MIT licensed.