API-First Integration: Consuming Third-Party APIs in Python

Part 2 of the Python for FDE track. Last updated: October 2026.

Every FDE engagement starts the same way: "here are the API docs, good luck." Your job is to turn a third-party API into reliable data your prototype can use. This post covers the full professional pattern — authentication, pagination, rate-limit handling, error classification, and persisting responses — the way you'd actually write it at a client site.

Setup: a session with auth baked in

Use a requests.Session so headers, auth, and connection pooling are configured once. Most client APIs use an API key header or a bearer token:

import requests

session = requests.Session()
session.headers.update({
    "Authorization": "Bearer " + "YOUR_BEARER_TOKEN",   # or "X-API-Key": "YOUR_API_KEY"
    "User-Agent": "fde-client-integration/1.0",
})

resp = session.get("https://api.client.com/v1/projects", timeout=10)
resp.raise_for_status()
print(resp.json()["data"][0]["name"])
# Acme rollout

OAuth2: the client-credentials flow

Enterprise APIs usually speak OAuth2. The client-credentials flow is the server-to-server variant — no user login, just your service proving who it is to get a short-lived token:

import time

import requests

_token, _token_expiry = None, 0

def get_token(session: requests.Session) -> str:
    """Fetch (and cache until expiry) an OAuth2 access token."""
    global _token, _token_expiry
    if _token and time.time() < _token_expiry - 60:   # reuse with 60s safety margin
        return _token
    r = session.post(
        "https://auth.client.com/oauth/token",
        data={"grant_type": "client_credentials",
              "client_id": "YOUR_CLIENT_ID",
              "client_secret": "YOUR_CLIENT_SECRET"},
        timeout=10,
    )
    r.raise_for_status()
    body = r.json()
    _token = body["access_token"]
    _token_expiry = time.time() + int(body.get("expires_in", 3600))
    return _token

Pagination: get everything, not just page one

Demos built on the first page of results are how you embarrass yourself in front of a client. Cursor pagination is the modern standard — loop until there is no next cursor:

def fetch_all(base_url: str, session: requests.Session) -> list:
    items, cursor = [], None
    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor
        r = session.get(base_url, params=params, timeout=15)
        r.raise_for_status()
        page = r.json()
        items.extend(page["data"])
        cursor = page.get("next_cursor")
        if not cursor:
            return items

projects = fetch_all("https://api.client.com/v1/projects", session)
print(f"Fetched {len(projects)} projects")
# Fetched 342 projects

Rate limits: back off gracefully

Hit a rate limit and the API returns 429. The professional response is exponential backoff — and honor the Retry-After header when the API sends one:

import time

def get_with_retry(session: requests.Session, url: str, tries: int = 5):
    for attempt in range(tries):
        r = session.get(url, timeout=15)
        if r.status_code == 429:
            wait = int(r.headers.get("Retry-After", 2 ** attempt))
            print(f"Rate limited — waiting {wait}s (attempt {attempt + 1})")
            time.sleep(wait)
            continue
        r.raise_for_status()
        return r
    raise RuntimeError(f"Still rate-limited after {tries} tries: {url}")

Error handling: classify before you retry

Not every failure deserves a retry. Classify by status code: retry timeouts and 5xx, refresh tokens on 401, fix your request on 4xx:

import requests

def fetch_project(session: requests.Session, project_id: str) -> dict:
    url = f"https://api.client.com/v1/projects/{project_id}"
    try:
        r = session.get(url, timeout=15)
        r.raise_for_status()
        return r.json()
    except requests.Timeout:
        print("Timed out — safe to retry")
        raise
    except requests.HTTPError as e:
        status = e.response.status_code
        if status in (408, 429) or 500 <= status < 600:
            print(f"Retryable {status} — back off and try again")
        elif status == 401:
            print("Unauthorized — refresh the token, don't retry blindly")
        elif status == 404:
            print("Not found — check the resource id")
        raise

Persist: save raw responses before you transform

Golden FDE rule: save the raw API response first, transform second. When the demo breaks at 9 AM, you want the original data on disk, not a second API call away. JSON for small payloads, NDJSON (one JSON object per line) for large ones:

import json
from pathlib import Path

Path("raw").mkdir(exist_ok=True)

projects = fetch_all("https://api.client.com/v1/projects", session)

Path("raw/projects.json").write_text(json.dumps(projects, indent=2))

with open("raw/projects.ndjson", "w") as f:
    for p in projects:
        f.write(json.dumps(p) + "\n")

print(f"Saved {len(projects)} raw records to raw/")
# Saved 342 raw records to raw/

Key takeaways

  • Configure auth once on a requests.Session — API key header, bearer token, or OAuth2 client-credentials with cached expiry.
  • Paginate everything — loop on the cursor until it's gone; first-page demos embarrass you.
  • On 429, back off exponentially and honor Retry-After.
  • Classify errors before retrying: retry 408/429/5xx, refresh tokens on 401, fix the request on other 4xx.
  • Save raw responses to disk first (JSON or NDJSON) — transform from the saved copy.

Next in this series: Building Client Prototypes Fast: Streamlit Dashboards in an Afternoon.

Comments

Popular posts from this blog

Java Banking Finance Services and Insurance (BFSI) domain interview questions

JSP Servlet Interview Questions For Freshers Series 1

Java program to check even or odd number