TechByteByByte

Working with APIs

Learn HTTP fundamentals, request and response handling, status codes, timeouts, and retries in Python, and how to call and parse real LLM APIs.

#Python#APIs#HTTP#REST#LLM APIs#AI#Python for AI

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/json header (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

CodeMeaningWhat it tells you
200OKRequest succeeded
400Bad RequestSomething’s wrong with what you sent (e.g., malformed JSON)
401UnauthorizedMissing or invalid API key
403ForbiddenYou don’t have permission for this action
404Not FoundThe URL/endpoint doesn’t exist
429Too Many RequestsYou’ve hit a rate limit — slow down and retry
500Internal Server ErrorSomething went wrong on the provider’s side
529OverloadedThe 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 JSONDecodeError Cloudflare 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 that response.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 Limit response. The names and units are provider-specific; these are examples you may encounter:

  • x-ratelimit-reset-requests: seconds until your request limit resets
  • x-ratelimit-reset-tokens: seconds until your token limit resets
  • retry-after: seconds to wait before trying again

In 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 Request or 401 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, 5xx server 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

  1. Write a function that sends a POST request (using a made-up URL) with a JSON body containing model, messages, and max_tokens.
  2. Write an if/elif chain that prints a specific, human-readable message for status codes 200, 401, 429, and any other code.
  3. Write a retry function using exponential backoff that retries up to 4 times, only on status code 429.
  4. Write a function parse_response(response_json) that safely extracts text and total_tokens (input + output) from a realistic Claude-style JSON response.
  5. Explain, in your own words, why a 401 error should never be retried, but a 429 error 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