The problem: Many hosted LLMs and model servers are reached through HTTP, but provider SDKs can hide the request-and-response mechanics. When a request times out or authentication fails, that hidden layer suddenly matters.
What you will learn: You will follow one API call from Python to a server and back through methods, URLs, headers, JSON bodies, and status codes. Then you will add timeouts, connection reuse, error handling, and safe retries. Providers differ in authentication, fields, streaming formats, and defaults, but they build on the same HTTP foundation.
1. What Is an API?
An API is a boundary between two programs. The caller does not reach into the server’s memory; it sends a request that follows an agreed contract and receives a response.
your Python code
↓ request: method + URL + headers + optional body
API server
↓ response: status + headers + body
your Python code validates and uses the result
The contract says which operations exist, what data they accept, how authentication works, and what responses mean. A successful network connection does not guarantee a successful operation, which is why code must check both transport errors and the response status.
A Contract Between Programs
API stands for Application Programming Interface — a defined way for one program to ask another program to do something, and get a response back.
Why Programs Need a Stable Boundary
You don’t have Anthropic’s or OpenAI’s actual model running on your laptop. The model runs on their servers. An API is the agreed-upon “contract” for how your code can ask their servers to run the model and send you the result.
Picture a Restaurant Order
An API is a restaurant menu and waiter combined. You (the client) don’t walk into the kitchen (the server’s internal code) and cook the food yourself. You order from a fixed menu (the API’s defined endpoints and parameters), the waiter (HTTP) carries your order to the kitchen, and carries the finished dish (the response) back to you.
Following One Request
Calling an LLM API is like calling a very well-organized international phone line: you dial a specific number (the URL), speak in an agreed format both sides understand (JSON), and get a specific, structured answer back — not a rambling conversation.
2. HTTP Basics
What Is It?
HTTP (HyperText Transfer Protocol) is the standard way computers exchange information over the internet — the same protocol your browser uses to load a webpage is what your Python code uses to call an AI API.
Anatomy of an HTTP exchange
sequenceDiagram
participant Client as Your Python Code
participant Server as AI Provider's Server
Note over Client: Build Request
Client->>Server: HTTP Request (Method, URL, Headers, Body)
Note over Server: Process / Run Model
Server->>Client: HTTP Response (Status Code, Headers, Body)
Note over Client: Parse JSON Output
Every request has:
- A method (what kind of action: GET, POST, …)
- A URL (where to send it)
- Headers (metadata: authentication, content type)
- Optionally, a body (the actual data being sent, usually JSON)
Every response has:
- A status code (did it work? what kind of problem, if any?)
- Headers
- A body (usually JSON, containing the actual result)
🤖 How Is This Used in AI? When you call client.messages.create(...)
using an SDK, the SDK is doing exactly this underneath: building an HTTP
request, sending it, and parsing the HTTP response — the SDK just saves
you from writing that plumbing by hand.
3. GET Requests
What Is It?
GET requests retrieve data — they ask a server “give me this,” and
typically don’t send a large body of data along with the request.
import requests
response = requests.get("https://api.github.com/repos/python/cpython")
data = response.json()
print(data["full_name"])
print(data["stargazers_count"])
Expected Output (approximate — real values change over time):
python/cpython
68000
🧠 Intuition: A GET request is like asking a librarian “can you tell
me the current status of this book?” — you’re not handing over new
information, just requesting existing information back.
🤖 How Is This Used in AI? Checking the status of an async/batch job,
listing available models, or fetching account/usage information from an
AI provider’s API often uses GET.
4. POST Requests
What Is It?
POST requests send data to be processed — you attach a body (usually
JSON) containing what you want the server to act on.
import requests
url = "https://api.anthropic.com/v1/messages"
headers = {
"x-api-key": "your_api_key_here",
"content-type": "application/json",
"anthropic-version": "2023-06-01",
}
body = {
"model": "claude-sonnet-4-6",
"max_tokens": 500,
"messages": [{"role": "user", "content": "What is an API?"}],
}
response = requests.post(url, headers=headers, json=body)
data = response.json()
print(data["content"][0]["text"])
🧠 Intuition
If GET is “tell me what you already have,” POST is “here’s a new piece
of work — please process it and give me the result.” Calling an LLM is
almost always a POST request — you’re sending a prompt to be processed,
not just retrieving something that already exists.
🤖 How Is This Used in AI? Every single “call the model” operation —
sending a prompt, requesting an embedding, asking for a completion — is a
POST request, because you’re always sending new data (your prompt) for
the server to actively process.
5. Request Headers
What Is It?
Headers are metadata attached to a request — information about the request, separate from its main content.
headers = {
"x-api-key": "your_api_key_here", # authentication
"content-type": "application/json", # "the body is JSON"
"anthropic-version": "2023-06-01", # which API version to use
}
🧠 Intuition: If the request body is the letter itself, headers are everything written on the envelope — who it’s from, how it should be handled, what format the letter is in.
🤖 How Is This Used in AI? Every LLM API call requires an authentication header carrying your API key — this is how the provider knows who’s calling, and who to bill.
⚠️ Common Beginner Mistake: Forgetting the
content-type: application/jsonheader (when not using a library that sets it automatically) — the server may fail to correctly parse a JSON body sent without it.
6. Query Parameters
What Is It?
Extra data attached directly to the URL, after a ?, usually used with
GET requests to filter or configure what’s returned.
import requests
response = requests.get(
"https://api.example.com/search",
params={"query": "python ai tutorials", "limit": 5}
)
# The actual URL requests builds:
# https://api.example.com/search?query=python+ai+tutorials&limit=5
print(response.url)
🧠 Intuition: Query parameters are like adding instructions to an address label — “deliver to this address, and leave it at the front desk, and only if someone’s there before 5pm.”
🤖 How Is This Used in AI? Some AI provider endpoints use query
parameters for pagination (e.g., listing past requests) or filtering
(e.g., ?model=gpt-4o-mini).
7. Request Body
What Is It?
The main payload of a request — for AI APIs, this is a JSON object carrying everything the model needs: the prompt, the model name, generation settings.
body = {
"model": "claude-sonnet-4-6",
"max_tokens": 300,
"temperature": 0.7,
"messages": [
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "Summarize what an API is in one sentence."}
]
}
🧠 Recall Module 7: this is exactly a Python dict, sent as JSON. Every concept from JSON (Module 7) and dictionaries (Module 2) directly powers this.
8. JSON API Responses
response_data = {
"id": "msg_01XYZ",
"model": "claude-sonnet-4-6",
"role": "assistant",
"content": [
{"type": "text", "text": "An API lets programs talk to each other using a defined format."}
],
"usage": {"input_tokens": 15, "output_tokens": 18}
}
answer = response_data["content"][0]["text"]
tokens_used = response_data["usage"]["input_tokens"] + response_data["usage"]["output_tokens"]
print(answer)
print(f"Total tokens used: {tokens_used}")
Expected Output:
An API lets programs talk to each other using a defined format.
Total tokens used: 33
🤖 This is the exact shape you will parse after every real Anthropic API
call — knowing to navigate ["content"][0]["text"] is a skill you’ll use
constantly.
9. Status Codes
| Code | Meaning | What it tells you |
|---|---|---|
200 | OK | Request succeeded |
400 | Bad Request | Something’s wrong with what you sent (e.g., malformed JSON) |
401 | Unauthorized | Missing or invalid API key |
403 | Forbidden | You don’t have permission for this action |
404 | Not Found | The URL/endpoint doesn’t exist |
429 | Too Many Requests | You’ve hit a rate limit — slow down and retry |
500 | Internal Server Error | Something went wrong on the provider’s side |
529 | Overloaded | The AI provider’s servers are temporarily overwhelmed |
response = requests.post(url, headers=headers, json=body)
if response.status_code == 200:
print("Success:", response.json())
elif response.status_code == 401:
print("Authentication failed — check your API key.")
elif response.status_code == 429:
print("Rate limited — back off and retry later.")
else:
print(f"Unexpected error: {response.status_code}")
🧠 Intuition
Status codes are a quick, standardized signal for what happened, before you even look at the response body — much faster than parsing JSON just to discover something went wrong.
🤖 How Is This Used in AI? Real production AI code branches heavily on
status codes: 429 triggers a backoff-and-retry (Module 6’s retry
pattern), 401 means the API key is misconfigured and should fail loudly
rather than retry, 529 (an Anthropic-specific code for an overloaded
server) means “try again shortly, this isn’t your fault.”
10. Using Python HTTP Libraries
The requests library is the standard, most common choice for
synchronous HTTP calls in Python:
pip install requests
import requests
response = requests.post(url, headers=headers, json=body)
print(response.status_code)
print(response.json())
🧠 Intuition: requests handles the low-level HTTP mechanics
(connections, encoding, headers) so you work with simple Python objects —
dicts in, dicts out — instead of raw network bytes.
💡 Why Use Official SDKs vs. Raw HTTP?
In practice, an official SDK is often convenient for a supported provider, but raw HTTP remains useful for learning, debugging, or calling a service without a suitable SDK.
Depending on the SDK and its version, it may handle:
- Connection Management: It reuses TCP connections (using pooling) to make sequential API calls much faster.
- Typed Responses: Instead of parsing raw dicts like
data["content"][0]["text"], it gives you typed Python objects with auto-complete in your editor:response.content[0].text. - Retries: Some SDKs retry selected rate-limit and server failures. Check the provider’s current defaults so your application does not accidentally layer its own retries on top.
- Streaming Support: It handles the low-level Server-Sent Events (SSE) protocol to yield tokens as they arrive, making it easy to build generators.
Understanding the raw HTTP layer means you know exactly what the SDK is doing for you behind the scenes, making it much easier to debug network timeouts or credential errors when they happen.
11. API Error Handling
Combining Module 6 (exceptions) with real API mechanics:
import requests
class APIError(Exception):
pass
class RateLimitError(APIError):
pass
class AuthenticationError(APIError):
pass
def call_ai_api(prompt):
try:
response = requests.post(
"https://api.anthropic.com/v1/messages",
headers={"x-api-key": "your_api_key_here", "anthropic-version": "2023-06-01"},
json={"model": "claude-sonnet-4-6", "max_tokens": 300,
"messages": [{"role": "user", "content": prompt}]},
timeout=10,
)
except requests.exceptions.Timeout:
raise APIError("Request timed out after 10 seconds")
except requests.exceptions.ConnectionError:
raise APIError("Could not connect to the API — check your network")
if response.status_code == 401:
raise AuthenticationError("Invalid API key")
elif response.status_code == 429:
raise RateLimitError("Rate limit exceeded")
elif response.status_code != 200:
raise APIError(f"Unexpected error: {response.status_code}")
return response.json()
🤖 This function is a realistic sketch of what sits underneath a production-grade AI SDK call — distinguishing network-level failures (timeouts, connection errors) from API-level failures (bad auth, rate limits) lets calling code react appropriately to each.
[!WARNING] The
JSONDecodeErrorCloudflare Trap When an LLM service is down or overloaded, proxy servers (like Cloudflare or AWS API Gateways) often intercept the request and return a 502 Bad Gateway or 504 Gateway Timeout error formatted as an HTML webpage, not JSON.If your code immediately runs
data = response.json()without checking, Python will crash with a confusing parser error:json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)To prevent this, always call
response.raise_for_status()first (which raises an exception for any 4xx or 5xx status codes), or verify thatresponse.headers.get("content-type")contains"application/json"before parsing the body!
12. Timeouts
import requests
try:
response = requests.post(url, headers=headers, json=body, timeout=5)
except requests.exceptions.Timeout:
print("The API call took too long — giving up.")
🧠 Intuition
Without a timeout, your program will wait indefinitely if a server never responds — a timeout is your program refusing to wait forever, and instead failing predictably so you can react (retry, alert, fall back).
🤖 How Is This Used in AI? LLM calls can occasionally hang. A sensible timeout (e.g., 30-60 seconds for a normal request) prevents one stuck API call from freezing an entire AI application or agent loop.
13. Retries
Combining Module 10’s decorator pattern with real API status codes:
import time
import requests
def call_with_retry(url, headers, body, max_retries=3):
for attempt in range(1, max_retries + 1):
response = requests.post(url, headers=headers, json=body, timeout=10)
if response.status_code == 200:
return response.json()
if response.status_code == 429:
wait_time = 2 ** attempt # exponential backoff: 2s, 4s, 8s...
print(f"Rate limited. Waiting {wait_time}s before retry {attempt}...")
time.sleep(wait_time)
continue
# Non-retryable error (e.g. 401, 400) — fail immediately
response.raise_for_status()
raise Exception("Max retries exceeded")
🧠 Intuition
Exponential backoff — waiting longer after each failed attempt (2s, then 4s, then 8s) — avoids hammering an already-struggling server with immediate retries, giving it time to recover.
🤖 How Is This Used in AI? Exponential backoff is a common foundation for retry behaviour in AI clients. Each SDK decides which errors it retries, how many attempts it allows, and whether it adds jitter, so check the version you use instead of assuming identical defaults.
[!IMPORTANT] Production Alert: Reading Rate Limit Headers Some APIs return headers with a suggested wait time or quota-reset details in a
429 Rate Limitresponse. The names and units are provider-specific; these are examples you may encounter:
x-ratelimit-reset-requests: seconds until your request limit resetsx-ratelimit-reset-tokens: seconds until your token limit resetsretry-after: seconds to wait before trying againIn production codebases, we extract these headers to wait exactly the amount of time requested by the server rather than guessing:
if response.status_code == 429: # Look for the server's suggested wait time, default to 5s if not present wait_time = int(response.headers.get("retry-after", 5)) time.sleep(wait_time)
⚠️ Common Beginner Mistake: Retrying every error type, including
400 Bad Requestor401 Unauthorized. These won’t succeed no matter how many times you retry — a malformed request stays malformed, and a bad API key stays bad. Only retry genuinely transient failures (timeouts,429,5xxserver errors).
14. Calling AI APIs (putting it together)
import os
import requests
from dotenv import load_dotenv # Module 8
load_dotenv()
def ask_claude(prompt: str, model: str = "claude-sonnet-4-6", max_tokens: int = 500) -> str:
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
raise ValueError("ANTHROPIC_API_KEY not found in environment")
response = requests.post(
"https://api.anthropic.com/v1/messages",
headers={
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
json={
"model": model,
"max_tokens": max_tokens,
"messages": [{"role": "user", "content": prompt}],
},
timeout=30,
)
response.raise_for_status()
data = response.json()
return data["content"][0]["text"]
# answer = ask_claude("Explain APIs in one sentence.")
# print(answer)
This single function pulls together every module so far: environment variables (8), dictionaries and JSON (2, 7), exceptions (6), functions and type hints (4, 10), and now HTTP (11).
15. Processing LLM Responses
def parse_claude_response(response_json: dict) -> dict:
"""Extract the useful pieces from a raw Claude API response."""
return {
"text": response_json["content"][0]["text"],
"input_tokens": response_json["usage"]["input_tokens"],
"output_tokens": response_json["usage"]["output_tokens"],
"stop_reason": response_json.get("stop_reason"),
}
raw_response = {
"content": [{"type": "text", "text": "APIs let programs communicate."}],
"usage": {"input_tokens": 12, "output_tokens": 6},
"stop_reason": "end_turn",
}
parsed = parse_claude_response(raw_response)
print(parsed)
Expected Output:
{'text': 'APIs let programs communicate.', 'input_tokens': 12, 'output_tokens': 6, 'stop_reason': 'end_turn'}
🤖 Writing a small “parse the response into exactly what my app needs” function like this is standard practice — it isolates the rest of your application from the raw API shape, so if the API changes slightly, you only need to update one function.
The Full Picture
Python application
↓
build request dict (model, messages, settings)
↓
HTTP POST request (headers + JSON body)
↓
LLM API (Anthropic / OpenAI / etc.)
↓
JSON response
↓
Python dictionary (parsed)
↓
Application logic (display answer, save to DB, trigger next step)
Every arrow in that diagram is something you now understand concretely — not as a magic black box, but as dictionaries, HTTP calls, status codes, and error handling you could write yourself.
What Happens During One Request?
Python code → find server address → open secure connection
→ send method, headers, and body
→ server processes request
→ receive status, headers, and body → Python object
These stages can fail separately. Production clients often set a shorter connect timeout for establishing the connection and a longer read timeout while waiting for an LLM response. Reusing a client or session also reuses connections, avoiding part of this setup on every request.
Safe Retries and Idempotency
An operation is idempotent when repeating the same request has the same intended effect as doing it once. Reading a resource is commonly safe to retry; creating a payment or allowing an agent to send a message may not be. For a state-changing API, use the provider’s idempotency-key feature when available.
Retry only transient failures, respect Retry-After when the service supplies
it, add a small random jitter so many clients do not retry together, and set
a maximum attempt count. Header names and retry behaviour differ among APIs, so
do not assume every provider returns the same rate-limit headers.
Module Summary
You now understand HTTP at the level real AI API calls operate on: GET vs. POST, headers, query parameters, JSON request/response bodies, status codes, and — critically — how to build resilient calling code with timeouts, retries with exponential backoff, and clear, specific error handling.
AI Connection
This module is how your Python code actually reaches an AI model.
Every SDK call you’ll ever make (client.messages.create(...)) is a
thin, convenient wrapper around exactly the HTTP mechanics you just
learned — which means when something goes wrong (a 429, a timeout, an
auth failure), you’ll know precisely what’s happening and how to handle
it, instead of treating the SDK as an unreadable black box.
Mini Practice
- Write a function that sends a
POSTrequest (using a made-up URL) with a JSON body containingmodel,messages, andmax_tokens. - Write an
if/elifchain that prints a specific, human-readable message for status codes200,401,429, and any other code. - Write a retry function using exponential backoff that retries up to 4
times, only on status code
429. - Write a function
parse_response(response_json)that safely extractstextandtotal_tokens(input + output) from a realistic Claude-style JSON response. - Explain, in your own words, why a
401error should never be retried, but a429error usually should be.
Next: Module 12 — Logging and Production Python — observability for AI pipelines, and what should (and should never) end up in a log.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed