promptdojo_

talking to apis — the call ai makes 100 times a day — step 1 of 8

The shape of an API call

Every AI-generated script that talks to a service does roughly this:

import httpx

response = httpx.get("https://api.example.com/users/7")
response.raise_for_status()        # crash on 4xx/5xx
data = response.json()              # parse the body as JSON
print(data["name"])

Four lines. AI writes them on autopilot. Your job is to read them and catch the places they break: forgetting the status check, and indexing into the JSON with the wrong key.

A call is a request (you send) and a response (you receive). The request has a method (GET to read, POST to create), a URL, headers (Authorization, Content-Type), and — for POST — a JSON body, which httpx takes as httpx.post(url, json={...}). The response has a status code, headers, and a body. That three-digit status is the most diagnostic field in the response, and it's where this lesson starts.

A note on this chapter: the browser can't make real network calls. So instead of hitting a real URL, we'll work with hardcoded JSON shapes that match what an API would actually return. The pattern is the same; we just skip the wire.

The three status families

Only three families matter for everyday work, identified by the first digit of the code:

  • 2xx — success. 200 OK is the workhorse. 201 Created comes back from a POST that made something new. Anything in the 200s means the request worked.

  • 4xx — client error. You are wrong. 400 Bad Request (your JSON was malformed). 401 Unauthorized (no/invalid auth). 403 Forbidden (auth was valid, but you can't access this). 404 Not Found (the URL or resource doesn't exist). 429 Too Many Requests (you hit the rate limit). The common thread: don't retry blindly. Fix the request.

  • 5xx — server error. They are wrong. 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout. Not your fault, and a retry might work in a few seconds.

The // 100 trick: integer-divide a status by 100 to get the family. 200 // 100 == 2, 404 // 100 == 4, 503 // 100 == 5. AI uses this exact pattern when bucketing responses.

A worked example

The editor on the right routes three hardcoded fake responses using exactly that trick. Three iterations, three branches:

ok: {'id': 7, 'name': 'maya'}
client error: user not found
server error: service unavailable

This is the mental model: ignore the exact code, look at the family, decide the behavior.

Where AI specifically gets this wrong

One: ignoring the status code entirely. The script does response.json() straight after the call. If the API returns 401 with a JSON error body, .json() actually succeeds and hands back {"error": "..."} — then your downstream code asks for data["name"] and dies with a KeyError six lines away from the real bug: nobody checked the status.

Two: treating 4xx and 5xx the same. A single if response.status_code != 200: return None hides the difference between "the user doesn't exist" (a normal business case) and "the upstream service is on fire" (a transient failure worth retrying). Family-based branching is the move.

Three: no timeout. httpx.get(url, timeout=None) — or the older requests.get(url), which has no default at all — means a hung server hangs your script forever. Set it explicitly: httpx.get(url, timeout=10.0).

Run the editor. Three fake responses get routed to three different branches based purely on the first digit of their status.