ap.Abhishek Patel

Search

Search notes, essays, projects, the library and the AI course. Use arrow keys to choose and Enter to open.

Lesson 3 of 5 30 min Beyond the syllabus

How the web talks: HTTP, REST and JSON

Read and send HTTP requests with curl and Python, interpret status codes, headers and JSON, and retry only the errors that waiting can fix.

Words in this lessonHTTP · Status code · HTTP header · JSON · REST · curl · Rate limit · API
HTTP
The set of rules programs use to send requests and responses over the web. Like a postal system with standard order slips and reply slips that every business understands.
Status code
A three-digit number at the top of every HTTP response saying what happened. Like the stamp on a reply letter: "Done", "We do not know you", "Too many orders today, wait".
HTTP header
A name and value attached to an HTTP request or response that describes the message. Like the details written on an envelope: sender, account number, language of the letter inside.
JSON
A plain-text format for structured data, made of named fields, lists, text, numbers, true, false and null. Like a standard order form whose boxes every supplier knows how to read.
REST
A common style for web APIs where URLs name things and HTTP methods say what to do to them. Like a well-run counter where every item has a ticket number and a fixed set of things you can ask for.
curl
A terminal program that sends HTTP requests and shows the responses. Like phoning a supplier directly to check something, instead of filling in the full order system.
Rate limit
A cap on how many requests or tokens an API will accept from you in a period of time. Like a supplier who takes only so many orders per hour and asks callers to ring back later.
API
A published doorway that lets one program ask another program to do something, in an agreed format. Like a bakery counter: you do not walk into the kitchen; you order in a set way and get your order back.
On this page
  1. Every API call is a letter and a reply
  2. One request, traced from start to finish
  3. The stamp: status codes
  4. Envelope details: headers
  5. The order inside: JSON
  6. Opening the hood: what the SDK sends to Claude
  7. REST: a convention for designing APIs
  8. The real code: HTTP from Python
  9. Where it breaks: Friday night, explained
  10. Try it yourself
  11. Common mistakes
  12. Recap
  13. Go deeper

Friday, 8:40 p.m., the busiest hour of the week for Crumb’s online orders. Arjun’s support bot is in its first week of live testing, drafting replies for the support team. Then the drafts stop. Every new email shows the same message in the review screen: “Sorry, something went wrong. Please try again later.”

Meera calls him. “Is the AI down? Are we being charged for this?”

Arjun opens the server logs on his phone. They are full of lines like these:

anthropic.RateLimitError: Error code: 429 - {'type': 'error', 'error': {'type': 'rate_limit_error', ...}}
anthropic.InternalServerError: Error code: 529 - {'type': 'error', 'error': {'type': 'overloaded_error', ...}}

He does not know what 429 means. Or 529. So he does what feels obvious: he edits the code to catch every error and immediately try again, up to ten times, and redeploys. Within two minutes the logs scroll so fast he cannot read them. Every failed request has become ten failed requests, arriving as fast as the server can send them. Nothing gets better.

At 9:30 Priya calls back from a wedding. She listens for thirty seconds. “429 is the provider asking you to slow down. You answered by shouting ten times faster. Turn the retry loop off. On Monday, I will teach you to read the letter before you reply to it.”

Important

The idea in one sentence: Programs on the internet talk by exchanging HTTP messages: a request (a method, a URL, headers and usually a JSON body) and a response (a status code that says what happened, headers, and a JSON body), and once you can read both halves, API errors stop being mysterious.

Every API call is a letter and a reply

On Monday, Priya draws two rectangles on the whiteboard. “Forget the internet. Think of Crumb’s head office ordering from our flour supplier by post.”

“Every order slip has the same parts. What you want done: order, cancel, check status. Where it goes: the supplier’s address and the right department. Some details on the envelope: who you are, your account number, which language the slip is written in. And the order itself, inside.”

“And the reply?”

“Same structure, plus one thing that matters more than anything else: a short code stamped at the top. Done. We do not know you. No such product. Too many orders today, wait an hour. Our warehouse is on fire, try again later. You read that stamp first, and it tells you what to do next.”

HTTP (HyperText Transfer Protocol) is that postal system for the internet. Your browser uses it to fetch web pages. Your Python code uses it to call Claude. The WhatsApp ordering line uses it to send Crumb new orders. An API (a doorway that lets programs ask other programs to do things) on the web is, almost always, a set of addresses that accept HTTP requests.

The two halves:

  • An HTTP request has a method (what you want done), a URL (where), headers (envelope details) and an optional body (the content).
  • An HTTP response has a status code (the stamp), headers, and a body.

One request, traced from start to finish

Priya makes it real with curl, a terminal program that sends HTTP requests and shows you exactly what comes back. Start with something public that needs no key: GitHub’s API, asking about the repository where Python itself is developed. The -i option means “include the response headers in the output”.

curl -i https://api.github.com/repos/python/cpython

Here is what came back (trimmed; your numbers will differ):

HTTP/2 200
content-type: application/json; charset=utf-8
x-ratelimit-limit: 60
x-ratelimit-remaining: 56

{
  "id": 81598961,
  "full_name": "python/cpython",
  "description": "The Python programming language",
  "language": "Python",
  "stargazers_count": 77814,
  "open_issues_count": 9246,
  "default_branch": "main",
  ...
}

Read it top to bottom, the way you would read a reply letter:

  • HTTP/2 200: the stamp. 200 means “OK, here is what you asked for”.
  • content-type: application/json: a header saying the body is written in JSON (more on that below).
  • x-ratelimit-limit: 60 and x-ratelimit-remaining: 56: GitHub telling you that without signing in you get 60 requests per hour, and you have used 4. Remember these; they are the polite version of a 429.
  • The body: the actual data, a structured description of the repository.

curl did not print the request, but it sent one. It looked like this:

GET /repos/python/cpython HTTP/2
Host: api.github.com
User-Agent: curl/8.7.1
Accept: */*

GET is the method (“give me”), /repos/python/cpython is the path, and the lines below are request headers. No body: a GET just asks.

Now change one character: ask for a repository that does not exist, https://api.github.com/repos/python/does-not-exist. The stamp becomes HTTP/2 404, “not found”, and the body is a small JSON error message instead of the data.

Note

Pause and think. Arjun’s Friday errors were 429 and 529. From the postal analogy alone, before reading on, which of these should he have retried, and how?

Show the answer

Both, but slowly, with growing waits. 429 is “too many orders, wait a while”: retrying is right, but only after waiting, ideally as long as the server says (many APIs send a retry-after header with the number of seconds). 529 is Anthropic’s “we are overloaded right now”, a temporary problem on their side, so waiting and retrying is also right. What was wrong was retrying immediately and ten times: that multiplied the load during the exact moment the provider was asking for less.

The stamp: status codes

Status codes are three-digit numbers, and the first digit tells you the family. You only need to know a handful by heart.

Code Meaning What to do
200 OK It worked; the body has your answer Read the body
201 Created It worked and something new was created Read the body
400 Bad Request Your request was malformed: missing field, wrong type, invalid JSON Fix your code; retrying will fail identically
401 Unauthorized “I do not know who you are”: missing, wrong or revoked key Fix the key; do not retry
403 Forbidden “I know who you are, and you may not do this” Check permissions; do not retry
404 Not Found No such address or resource Check the URL or ID; do not retry
413 Payload Too Large Request body too big (say, a huge document) Send less
429 Too Many Requests You hit a rate limit Wait (respect retry-after), then retry
500 Internal Server Error Something broke on their side Retry after a pause
503 Service Unavailable Temporarily down or overloaded Retry after a pause
529 Overloaded Anthropic-specific: their servers are overloaded Retry after a pause

The big rule: 4xx means you did something wrong, 5xx means they did. Most 4xx errors will fail the same way forever, so retrying them is pointless; fix the request. The exception is 429, which is “you did nothing wrong except go too fast”. 5xx errors are usually temporary, so a few patient retries make sense.

Envelope details: headers

Headers are name: value lines that carry information about the message. A few you will meet constantly:

  • content-type: the format of the body. For AI APIs, almost always application/json.
  • authorization or x-api-key: who you are. OpenAI expects Authorization: Bearer sk-...; Anthropic expects x-api-key: sk-ant-.... Different providers, same job: putting your API key on the envelope.
  • retry-after: on a 429 or 503, how many seconds the server wants you to wait.
  • Provider-specific ones, like Anthropic’s anthropic-version, which pins the version of the API format you are using, so your code does not break when they change it.

The order inside: JSON

The body of almost every AI API request and response is JSON (JavaScript Object Notation): a plain-text format for structured data that both people and programs can read. It has only a few building blocks:

{
  "order_id": "CR-20931",
  "total_inr": 1450,
  "paid": true,
  "delivered_at": null,
  "items": ["Diwali hamper", "Plum cake"],
  "customer": {"name": "Kavya", "area": "Baner"}
}
  • { } an object: named fields, like a Python dictionary.
  • [ ] an array: an ordered list, like a Python list.
  • Strings in double quotes (always double, never single), numbers, true, false, and null (nothing; Python’s None).

That is all of JSON. In Python, json.loads(text) turns JSON text into dictionaries and lists, and json.dumps(data) goes the other way. HTTP libraries do it for you.

Opening the hood: what the SDK sends to Claude

Here is the part that changed how Arjun saw his bot. When his code calls client.messages.create(...), the Anthropic SDK (the ready-made Python library for the API) builds an HTTP request and sends it. You can send the very same request with curl:

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-opus-5-5",
    "max_tokens": 200,
    "messages": [{"role": "user", "content": "Do you have eggless cakes? One sentence."}]
  }'

The method is POST (“here is something for you to process”; curl uses POST automatically when you send data with -d). The URL ends in /v1/messages. Three headers: the key, the API version and the body format. And a JSON body with the model, a length cap and the conversation. The \ at the end of each line just continues the command on the next line (in PowerShell, use the Python version below instead).

The response body:

{
  "id": "msg_01XFDUDYJgAACzvnptvVoYEL",
  "type": "message",
  "role": "assistant",
  "model": "claude-opus-5-5",
  "content": [{"type": "text", "text": "Yes, we offer a range of eggless cakes..."}],
  "stop_reason": "end_turn",
  "usage": {"input_tokens": 19, "output_tokens": 23}
}

Every field is something you will use later in the course. content is a list of blocks (later you will see tool-call blocks here, in Module 06). stop_reason says why the model stopped: end_turn means it finished naturally; max_tokens means your cap cut it off. usage counts the tokens (small chunks of text) that you are billed for.

And with a wrong key, the response is a 401, exactly what Priya got when she tried it:

HTTP/2 401
content-type: application/json

{"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}

Note

Pause and think. Some older APIs let you put the key in the URL, like https://api.example.com/data?key=sk-123. Why do modern APIs insist on a header instead?

Show the answer

Because URLs get recorded in many places. Web servers, proxies and monitoring tools routinely log full URLs; browsers save them in history; people paste them into chats and bug reports. A key in a URL leaks through all of those. Headers are not normally logged or shared that way. (The same caution applies to your own logs: never log request headers wholesale, or your key ends up in a log file.)

REST: a convention for designing APIs

Most web APIs you will call follow a style called REST (Representational State Transfer). The name is academic; the idea is simple. The URL names a thing (a “resource”), and the HTTP method says what to do to it:

Method Meaning Crumb example
GET Read it GET /orders/CR-20931: fetch one order
POST Create something new, or ask for processing POST /orders: place an order (body holds the items)
PUT / PATCH Replace / partly update it PATCH /orders/CR-20931: change the delivery address
DELETE Remove it DELETE /orders/CR-20931: cancel

Two more REST habits: each request carries everything needed to understand it (the server does not remember your previous request; this is called being stateless), and responses use proper status codes. The statelessness will matter a lot in Module 02: LLM APIs are stateless too, which is why your chatbot must re-send the whole conversation every time. In Module 05 you will build REST APIs of your own with FastAPI, and Crumb’s support bot will become one.

The real code: HTTP from Python

curl is perfect for poking at an API. In your app you will use Python. Add HTTPX, a modern HTTP library, to the crumb-lab project from the setup lab:

uv add httpx

Step 1: a GET request. Create http_demo.py:

import httpx

r = httpx.get("https://api.github.com/repos/python/cpython", timeout=10)

print(r.status_code)              # the stamp
print(r.headers["content-type"])  # a header
repo = r.json()                   # parse the JSON body into a dict
print(repo["full_name"], repo["stargazers_count"])

Run uv run http_demo.py. You should see:

200
application/json; charset=utf-8
python/cpython 77814

timeout=10 means “give up after 10 seconds”. Always set one; without it, a hung server can freeze your program.

Step 2: handle failure deliberately. Change the URL to the nonexistent repository and add one line before r.json():

r.raise_for_status()  # turns 4xx and 5xx responses into a Python exception

You should now see an httpx.HTTPStatusError naming 404 Not Found. Raising on errors is better than reading the body blindly: a 404’s body is an error message, not repository data, and your code would crash later with a confusing KeyError.

Step 3: the same Claude request, from Python. This is what the SDK does for you, written out by hand so you can see it:

import os

import httpx
from dotenv import load_dotenv

load_dotenv()

r = httpx.post(
    "https://api.anthropic.com/v1/messages",
    headers={
        "x-api-key": os.environ["ANTHROPIC_API_KEY"],
        "anthropic-version": "2023-06-01",
        "content-type": "application/json",
    },
    json={  # httpx turns this dict into a JSON body
        "model": "claude-opus-5-5",
        "max_tokens": 200,
        "messages": [{"role": "user", "content": "Do you have eggless cakes? One sentence."}],
    },
    timeout=60,
)
r.raise_for_status()
data = r.json()
print(data["content"][0]["text"])
print(data["usage"])

You should see a one-sentence answer and something like {'input_tokens': 19, 'output_tokens': 23}. Now you know what the SDK’s five lines are really doing. In practice, use the SDK: it adds helpful types, clearer errors and automatic retries. But when something goes wrong, you will now recognise the HTTP underneath.

Where it breaks: Friday night, explained

On Monday, Priya and Arjun replay Friday using what he now knows.

Why the 429s started. A bug in Arjun’s review screen sent every email to the API three times. At 8:40 p.m., with the evening rush, that tripled request rate crossed Crumb’s account limit, and the API started answering 429. A few minutes later the provider was also overloaded (529) for everyone, which happens at busy times.

Why his fix made it worse. Immediate retries turned each failure into ten requests in under a second. That kept Crumb’s rate permanently over the limit, so almost every request kept failing. It is like a crowd at the counter: if everyone who is told “please wait” immediately shouts their order again, the queue never clears.

What good retrying looks like. Retry only what can succeed later (429 and 5xx), wait longer after each failure (1 second, then 2, then 4: exponential backoff), respect retry-after when the server sends it, and give up after a few tries with an honest message. A sketch:

import time

RETRYABLE = {429, 500, 502, 503, 529}

for attempt in range(4):
    r = httpx.post(url, headers=headers, json=body, timeout=60)
    if r.status_code not in RETRYABLE:
        break
    wait = float(r.headers.get("retry-after", 2 ** attempt))  # 1, 2, 4, 8 seconds
    print(f"Got {r.status_code}, waiting {wait}s")
    time.sleep(wait)

r.raise_for_status()  # still failing after 4 tries: raise, and show a friendly message upstream

The Anthropic and OpenAI SDKs already retry a couple of times with backoff by default, which is one more reason to use them. Arjun’s ten-times loop was on top of the SDK’s own retries. Module 19 covers retries, fallbacks to other providers and rate limits properly.

Other ways HTTP bites AI apps. Timeouts: long model answers can take a minute, so set timeouts generously but deliberately. Huge requests: pasting a 300-page PDF into one request can return a 400 or 413. Bad JSON: when you ask a model to reply in JSON, it may return almost-JSON with a stray sentence in front, and json.loads will fail (Module 05 fixes this with structured outputs). And keys in shell history: the curl example above uses $ANTHROPIC_API_KEY rather than the literal key, so the key never lands in your terminal history.

Try it yourself

Using curl (or the Python script from Step 1 if you are on Windows):

  1. Fetch https://api.github.com/repos/pallets/flask and find the value of "language" and the x-ratelimit-remaining header.
  2. Fetch the same URL with a typo in the repository name. What status code do you get?
  3. For each situation, say whether you would retry: (a) 401 from Claude, (b) 503 from OpenAI, © 400 complaining max_tokens is missing, (d) 429 with retry-after: 20.
Show the answer
  1. curl -i https://api.github.com/repos/pallets/flask shows "language": "Python", and x-ratelimit-remaining is one less than last time you checked (it counts down from 60 per hour).
  2. 404, with a small JSON body saying “Not Found”.
  3. (a) No: fix the key; it will fail forever. (b) Yes, after a pause; the server is temporarily unavailable. © No: your request is malformed; fix the code. (d) Yes, but only after waiting 20 seconds.

Common mistakes

  • Retrying everything. Retrying a 401 or 400 just repeats the failure. Retry 429 and 5xx, with growing waits.
  • Reading the body before checking the status. A 404 or 500 body is an error message, not your data. Check status_code or call raise_for_status() first.
  • No timeout. One hung connection can freeze a whole worker. Always pass timeout=.
  • Single quotes in JSON. {'a': 1} is a Python dictionary, not JSON. JSON needs double quotes.
  • Logging full requests. The API key is in the headers. Log the status, the URL and the timing, never the auth header.

Recap

  • HTTP is a request and a response. A request has a method, URL, headers and optional body; a response has a status code, headers and a body.
  • Status codes: 2xx success, 4xx your mistake (do not retry, except 429), 5xx their problem (retry patiently).
  • Headers carry the API key, the body format and hints like retry-after. JSON carries the data.
  • REST APIs name things in URLs and use methods (GET, POST, PATCH, DELETE) as verbs, and are stateless.
  • The SDK is a friendly wrapper around exactly these HTTP requests; knowing what is underneath turns errors into instructions.

On Monday afternoon, Meera asks whether Friday was the AI’s fault.

“Half ours, half theirs,” Arjun says. “Every time our bot asks the AI something, it sends a message and gets a reply with a number on top, like a stamp. On Friday the stamp said 429: ‘you are sending too much, slow down’. That was our bug, sending everything three times. Then their servers got busy, which is the 529. I made it worse by asking again immediately, ten times. Now we only retry when the stamp says waiting will help, and we wait a little longer each time.”

“Did it cost us money?”

“Failed requests are not billed. The bug that sent every email three times would have, though. That is fixed too.”

Priya nods. “Good. Next problem: Module 10 needs Redis, Module 20 needs Ollama, Module 08 needs Milvus. All servers. You are not installing three databases on your laptop by hand. Next lesson: Docker.”

Go deeper

Check your understanding

3 quick questions
1. Which of these responses should you NOT retry?
2. What does a status code in the 5xx family usually mean?
3. Why did Arjun's "retry immediately, ten times" fix make Friday night worse?