{"post":{"slug":"how-to-work-with-apis-2026","title":"How to work with APIs in 2026: HTTP, auth, errors, and real patterns","excerpt":"Working with APIs is the most common programming task in 2026, yet most tutorials stop at the happy path. This guide covers auth, error handling, rate limits, and building reliable API clients.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">A</span>PI integration is the default task of modern software development. Payment processors, AI models, mapping services, identity providers, data pipelines — virtually every feature you build in 2026 calls an external API. The difference between brittle integrations that page you at 3am and reliable ones that just work comes down to a handful of patterns that most tutorials skip. Here they are.\n\n## What changed in 2026\n\n- **`httpx` is the standard HTTP client** — `requests` still works but lacks native async support; most new Python code uses `httpx`.\n- **OpenAPI 3.1 and typed SDK generation is universal** — major API providers ship auto-generated Python SDKs. Use the SDK when available; drop to raw HTTP only when you need control.\n- **OAuth 2.0 PKCE is required for browser/mobile flows** — the implicit flow is deprecated across all major providers.\n- **API versioning via URL path vs header is settled** — path versioning (`/v2/`) won; header-based versioning (`X-API-Version: 2`) is rare in new APIs.\n- **LLM APIs (OpenAI, Anthropic, etc.) are first-class targets** — streaming responses via Server-Sent Events (SSE) are now a standard pattern to know.\n\n## The four auth patterns\n\n| Pattern | How it works | When used |\n| --- | --- | --- |\n| API Key | Static secret in header or query param | Server-to-server, simple integrations |\n| OAuth 2.0 + PKCE | Token exchange, user consent | Third-party user data (Google, GitHub) |\n| JWT (Bearer token) | Signed token, self-contained claims | Internal services, session auth |\n| mTLS | Client certificate validation | High-security B2B APIs |\n\n## Making basic API calls with httpx\n\n```python\nimport httpx\n\n# Synchronous — simple scripts\nwith httpx.Client(timeout=10) as client:\n    response = client.get(\n        \"https://api.example.com/v2/users/me\",\n        headers={\"Authorization\": \"Bearer sk-your-token\"},\n    )\n    response.raise_for_status()   # raises HTTPStatusError on 4xx/5xx\n    user = response.json()\n\n# Async — FastAPI, async scripts, concurrent calls\nimport asyncio\n\nasync def fetch_user(client: httpx.AsyncClient, user_id: str) -> dict:\n    response = await client.get(f\"/users/{user_id}\")\n    response.raise_for_status()\n    return response.json()\n\nasync def main():\n    async with httpx.AsyncClient(base_url=\"https://api.example.com\") as client:\n        # Fetch 10 users concurrently\n        tasks = [fetch_user(client, str(i)) for i in range(1, 11)]\n        users = await asyncio.gather(*tasks)\n```\n\n## Retry with exponential backoff — the survival pattern\n\n```python\nimport httpx\nimport time\nimport random\n\ndef api_get_with_retry(url: str, headers: dict, max_retries: int = 3) -> dict:\n    for attempt in range(max_retries):\n        try:\n            response = httpx.get(url, headers=headers, timeout=10)\n\n            if response.status_code == 429:\n                retry_after = int(response.headers.get(\"Retry-After\", 60))\n                time.sleep(retry_after)\n                continue\n\n            response.raise_for_status()\n            return response.json()\n\n        except httpx.TransportError:\n            if attempt == max_retries - 1:\n                raise\n            backoff = (2 ** attempt) + random.uniform(0, 1)\n            time.sleep(backoff)\n\n    raise RuntimeError(\"Max retries exceeded\")\n```\n\nOr use `tenacity` for production-grade retry logic:\n\n```python\nfrom tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type\n\n@retry(\n    stop=stop_after_attempt(3),\n    wait=wait_exponential(multiplier=1, min=1, max=60),\n    retry=retry_if_exception_type(httpx.TransportError),\n)\ndef fetch_data(url: str) -> dict:\n    response = httpx.get(url, timeout=10)\n    response.raise_for_status()\n    return response.json()\n```\n\n## Pagination patterns\n\n```python\ndef paginate_cursor(base_url: str, headers: dict):\n    \"\"\"Cursor-based pagination — most modern APIs use this.\"\"\"\n    cursor = None\n    while True:\n        params = {\"limit\": 100}\n        if cursor:\n            params[\"after\"] = cursor\n\n        response = httpx.get(base_url, params=params, headers=headers)\n        response.raise_for_status()\n        data = response.json()\n\n        yield from data[\"items\"]\n\n        cursor = data.get(\"next_cursor\")\n        if not cursor:\n            break\n\ndef paginate_offset(base_url: str, headers: dict):\n    \"\"\"Offset-based pagination — older REST APIs.\"\"\"\n    page, per_page = 1, 100\n    while True:\n        response = httpx.get(base_url, params={\"page\": page, \"per_page\": per_page}, headers=headers)\n        response.raise_for_status()\n        items = response.json()\n        if not items:\n            break\n        yield from items\n        page += 1\n```\n\n## Streaming API responses (SSE / LLMs)\n\n```python\nimport httpx\n\ndef stream_llm_response(prompt: str, api_key: str):\n    \"\"\"Stream tokens from an LLM API using Server-Sent Events.\"\"\"\n    with httpx.stream(\n        \"POST\",\n        \"https://api.anthropic.com/v1/messages\",\n        headers={\"x-api-key\": api_key, \"anthropic-version\": \"2023-06-01\"},\n        json={\n            \"model\": \"claude-sonnet-4-6\",\n            \"max_tokens\": 1024,\n            \"stream\": True,\n            \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n        },\n        timeout=60,\n    ) as response:\n        for line in response.iter_lines():\n            if line.startswith(\"data: \"):\n                yield line[6:]   # strip \"data: \" prefix\n```\n\n## API response error taxonomy\n\n| Status | Meaning | Action |\n| --- | --- | --- |\n| 400 Bad Request | Your request is malformed | Fix the request; log the body |\n| 401 Unauthorized | Missing or invalid credentials | Refresh token or check API key |\n| 403 Forbidden | Authenticated but not allowed | Check scopes/permissions; not retriable |\n| 404 Not Found | Resource does not exist | Check the ID; not retriable |\n| 422 Unprocessable | Validation error | Fix the payload; log the error body |\n| 429 Too Many Requests | Rate limited | Back off per Retry-After header |\n| 500/503 Server Error | Their bug | Retry with backoff; alert if persistent |\n\n## How to build a production API client\n\n1. **Use a base client with shared config** — base URL, auth headers, timeout, retry.\n2. **Type your responses** — Pydantic models for every response shape you parse.\n3. **Log request/response metadata** — URL, status, latency, request ID headers.\n4. **Test with a mock** — use `pytest-httpx` or `respx` to mock httpx calls in tests.\n5. **Handle pagination exhaustively** — assume any endpoint could have 1M records.\n\n## Common mistakes\n\n**No timeout.** `httpx.get(url)` without a timeout will hang forever on a stalled connection. Always pass `timeout=10` (or higher for slow endpoints).\n\n**Ignoring error response bodies.** `response.raise_for_status()` throws but discards the body. Log `response.text` or `response.json()` before raising — error details are in the body.\n\n**Storing API keys in code.** Use environment variables or a secrets manager. Never commit API keys to git.\n\n**Sequential calls when concurrent is possible.** Fetching 100 records one by one is 100x slower than using `asyncio.gather` with an async client. Profile before optimizing, but concurrent fetching is almost always the right choice.\n\n**Not versioning API calls.** Hardcoding `/v1/` everywhere without a constant means updating the version requires touching every file.\n\n## What to skip\n\n- **`urllib` directly** — it is low-level and verbose; `httpx` handles encoding, redirects, and connection pooling correctly.\n- **Custom session management** — `httpx.Client` with a context manager handles connection pooling. Do not reimplement it.\n- **Polling when webhooks exist** — if the API offers webhooks, receive push events rather than polling every N seconds.\n\n## FAQ\n\n**`requests` or `httpx` in 2026?**\n`httpx` for new code — it is a near-drop-in replacement with async support, HTTP/2, and an actively maintained codebase. Use `requests` only when you need a plugin from its ecosystem that `httpx` does not have.\n\n**How do I test code that calls external APIs?**\nUse `respx` (for httpx) or `responses` (for requests) to mock HTTP calls in unit tests. For integration tests, record real responses with VCR cassettes.\n\n**What is the best way to manage API keys securely?**\nEnvironment variables loaded via `python-dotenv` locally, and a secrets manager (AWS Secrets Manager, GCP Secret Manager, Doppler) in production. Never hardcode in source.\n\n**How do I handle API versioning when the provider ships a new version?**\nPin to the current version in a central config constant. When the provider releases v2, audit breaking changes, update the constant, and test. Never let the version drift silently.\n\n## Where to go next\n\n- [How to parse JSON in 2026](/blog/how-to-parse-json-2026)\n- [How to handle API errors in 2026](/blog/how-to-handle-api-errors-2026)\n- [How to build a REST API in 2026](/blog/how-to-build-a-rest-api-2026)","hue":312,"metaTitle":"How to Work With APIs in 2026: Auth, Errors, Rate Limits","metaDescription":"Complete 2026 guide to working with REST and GraphQL APIs in Python. Covers httpx, authentication patterns, error handling, rate limiting, pagination","keywords":["how to work with apis 2026","python api tutorial 2026","httpx tutorial python","rest api python 2026","api authentication patterns","oauth 2.0 python tutorial","api error handling python","rate limiting api python","api pagination python","building api client python 2026"],"tldr":["**Use `httpx` for HTTP in Python 2026** — it supports async, HTTP/2, and has a cleaner API than `requests`; use `requests` only for legacy codebases.","**Always handle errors explicitly** — check status codes, catch network exceptions, and implement retry with exponential backoff for transient failures.","**Understand the four auth patterns** — API keys, OAuth 2.0, JWT, and mTLS; most APIs use one of these; know when each applies.","**Rate limits are constraints, not suggestions** — 429 responses require backoff; ignoring them gets your key banned.","**Skip:** building your own HTTP client from `socket` — `httpx` and its ecosystem handle connection pooling, redirects, TLS, and retries correctly."],"readTime":"5 min read","wordCount":1175},"related":[{"slug":"best-api-design-practices-2026","title":"Best API design practices in 2026: build APIs people enjoy using","excerpt":"A practical guide to designing APIs in 2026 - REST versus alternatives, versioning, pagination, errors, and the conventions that keep clients happy.","category":"code","date":"June 3, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":205},{"slug":"rest-api-best-practices-2026","title":"REST API best practices in 2026: design, security, and versioning","excerpt":"A poorly designed REST API is harder to fix than bad app code because clients depend on it. Here are the 2026 best practices for APIs that age well.","category":"code","date":"March 19, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":305},{"slug":"how-to-build-a-rest-api-in-node-2026","title":"How to build a REST API in Node in 2026: the complete guide","excerpt":"Building a production-ready REST API in Node.js in 2026 means choosing the right framework, structuring routes cleanly, and shipping validation and auth from day one.","category":"code","date":"March 11, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":318}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-wake-up-early-2026","title":"How to wake up early in 2026: fix the morning by fixing the evening","excerpt":"Waking up early is a sleep architecture problem, not a willpower problem. Get the evening right and early mornings follow. Here is the exact protocol.","category":"guides","date":"March 18, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":300},"older":{"slug":"how-to-write-a-migration-script-2026","title":"How to write a migration script in 2026","excerpt":"Migration scripts move data safely between schemas or systems without downtime. This guide covers patterns, tooling, and rollback strategies for 2026.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":313}}