Python httpx GET and POST with Headers, Parameters, and JSON
Learn how to make GET and POST requests with Python httpx, including headers, query parameters, JSON bodies, response handling, timeouts, and async requests.
When you need to interact with an HTTP API from Python, httpx offers a modern, feature-rich client that handles GET and POST requests cleanly. This article covers how to set headers, pass query parameters, send JSON payloads, and interpret responses correctly.
Making a GET Request with Headers and Query Parameters
A GET request often needs custom headers (e.g., authentication tokens) and query parameters to filter or paginate results. httpx accepts both through dedicated arguments.
import httpx headers = { "Authorization": "Bearer your-token", "Accept": "application/json", } params = { "page": 2, "limit": 50, "status": "active", } response = httpx.get( "https://api.example.com/users", headers=headers, params=params, ) print(response.status_code) print(response.json())
The params argument serializes the dictionary into a query string automatically. httpx handles URL encoding, so values with spaces or special characters are escaped correctly. If you need to send the same parameter multiple times, pass a list of values instead of a single string.
params = {"id": [1, 2, 3]} # Results in ?id=1&id=2&id=3
Headers are passed as a plain dictionary. Header names are case-insensitive, so you do not need to match a specific casing.
Sending a POST Request with a JSON Body
POST requests typically carry a JSON payload. httpx provides the json argument, which automatically serializes a Python dictionary to JSON and sets the Content-Type header to application/json.
import httpx payload = { "name": "Ada Lovelace", "role": "analyst", "active": True, } response = httpx.post( "https://api.example.com/users", json=payload, headers={"Authorization": "Bearer your-token"}, ) print(response.status_code) print(response.json())
Using json= is the most straightforward way for JSON APIs. It avoids manual serialization and header management. The request body is encoded to UTF-8, and the Content-Type header is set for you.
Choosing Between params, data, and json
httpx distinguishes between several ways to send request data. Understanding the difference prevents subtle bugs in API integrations.
| Argument | Use case | Content-Type | Example body |
|---|---|---|---|
params | Query string in the URL | None (appended to URL) | ?page=2&limit=50 |
data | Form-encoded fields | application/x-www-form-urlencoded | name=Ada&role=analyst |
json | JSON payload | application/json | {"name":"Ada"} |
content | Raw request body bytes | Depends on the headers you set | b"raw bytes" |
Use params for filtering and pagination that belong in the URL. Use json when the API expects a JSON object. Use data when you need to send form fields. Use content when you need to send raw bytes as the request body.
# Form-encoded POST response = httpx.post("https://httpbin.org/post", data={"key": "value"}) # Raw bytes POST with open("file.bin", "rb") as f: response = httpx.post("https://upload.example.com", content=f.read())
data and json are mutually exclusive; passing both raises an error. params, on the other hand, merges with any query string already present in the URL. If the same key appears in both places, the params value takes precedence, so keep query parameters in params rather than mixing them into the URL string.
Reading Response Content and Status Codes
After a request completes, the Response object gives you access to the status code, headers, and body. The most common pattern is to check the status before parsing the body.
response = httpx.get("https://api.example.com/status") if response.status_code == 200: data = response.json() else: print(f"Request failed: {response.status_code}")
response.json() parses the body as JSON. If the body is not valid JSON, it raises json.JSONDecodeError. For non-JSON responses, use response.text for a string or response.content for raw bytes.
httpx also exposes response.headers, a case-insensitive mapping of response headers. This is useful when the API returns pagination links or rate-limit information.
rate_limit_remaining = response.headers.get("X-RateLimit-Remaining")
Handling Errors, Timeouts, and Retries
Network requests fail for many reasons: timeouts, connection errors, or invalid status codes. httpx raises httpx.RequestError for network-level failures and httpx.HTTPStatusError for 4xx/5xx responses when you call response.raise_for_status().
import httpx url = "https://api.example.com/data" try: response = httpx.get( url, timeout=5.0, headers={"Authorization": "Bearer token"}, ) response.raise_for_status() except httpx.TimeoutException: print("The request timed out.") except httpx.HTTPStatusError as exc: print(f"HTTP error: {exc.response.status_code}") except httpx.RequestError as exc: print(f"Network error: {exc}")
Set a timeout on every request to avoid hanging indefinitely. httpx supports separate timeouts for connect, read, write, and pool operations via a Timeout object, but a single float is sufficient for most scripts.
timeout = httpx.Timeout(5.0, connect=2.0) response = httpx.get(url, timeout=timeout)
For connection-level retries, httpx.HTTPTransport accepts a retries argument. This retries failed connection attempts, not requests that return a 4xx or 5xx response. If you need application-level retry policies, implement a loop or use a library like tenacity. Keep in mind that retrying a POST request can duplicate side effects unless the API is idempotent.
Reusing Clients for Connection Pooling and Performance
Creating a new httpx.Client for every request prevents connection reuse and repeats TLS handshakes. For scripts that make multiple requests to the same host, reuse a single client instance.
import httpx with httpx.Client( base_url="https://api.example.com", headers={"Authorization": "Bearer token"}, timeout=10.0, ) as client: response1 = client.get("/users", params={"page": 1}) response2 = client.post("/users", json={"name": "Grace"})
A Client maintains a connection pool, reuses TCP connections, and applies default headers and timeouts to every request. This reduces latency and CPU usage when you issue many requests in a loop or a batch job.
The base_url argument lets you use relative paths, which keeps code concise and avoids repeating the origin. If you need different headers for a specific request, pass them at the call site; they are merged with the client defaults.
Async Requests with httpx
For concurrent workloads, httpx provides an async API that mirrors the sync interface. Use httpx.AsyncClient with await inside an async function.
import httpx import asyncio async def fetch_status(client: httpx.AsyncClient, url: str) -> int: response = await client.get(url) return response.status_code async def main(): urls = ["https://example.com", "https://httpbin.org/get"] async with httpx.AsyncClient() as client: results = await asyncio.gather(*(fetch_status(client, url) for url in urls)) print(results) asyncio.run(main())
Async requests are useful when you need to issue many independent requests concurrently, such as checking multiple health endpoints or fetching data from several services. The async client also supports connection pooling, but it requires an event loop and is not a drop-in replacement for sync code in a regular script.
Where Configuration Commonly Breaks
A frequent mistake is mixing params with a URL that already contains a query string. httpx merges them, but if the same key appears in both, the params value takes precedence. This can lead to unexpected behavior when the URL is generated dynamically.
Another issue is sending a dict with data= when the API expects JSON. This results in a form-encoded body, and the server may reject it or parse it incorrectly. Always match the argument to the API's expected content type.
Finally, remember that response.json() does not verify the status code. A 404 response might return a JSON error object, and your code could process it as if it were successful data. Always check response.status_code or call raise_for_status() before parsing the body.
These patterns cover the core of using httpx for GET and POST requests with headers, parameters, and JSON bodies. By using the correct arguments and understanding response behavior, you can build reliable HTTP clients for any API integration.