How to Send POST JSON with Python Requests (2026)
Complete guide to sending JSON data with Python's requests library
Start Building with Hypereal
Access Kling, Flux, Sora, Veo & more through a single API. Free credits to start, scale to millions.
No credit card required • 100k+ developers • Enterprise ready
How to Send POST JSON with Python Requests (2026)
The Python requests library is the most popular HTTP library in the Python ecosystem, and sending POST requests with JSON data is one of its most common use cases. Whether you are calling a REST API, sending data to a webhook, or integrating with a third-party service, you need to know the right way to structure your request.
This guide covers everything from the basics to advanced patterns including authentication, error handling, retries, and async requests.
The Basics: Sending a JSON POST Request
Method 1: Using the `json` Parameter (Recommended)
The simplest and most correct way to send JSON data:
import requests
url = "https://api.example.com/v1/users"
data = {
"name": "Jane Doe",
"email": "jane@example.com",
"role": "developer"
}
response = requests.post(url, json=data)
print(response.status_code) # 200
print(response.json()) # Parse JSON response
When you use the json parameter, requests automatically:
- Serializes the dictionary to a JSON string
- Sets the
Content-Typeheader toapplication/json - Encodes the data as UTF-8
Method 2: Using `data` with Manual Serialization
If you need more control over the serialization:
import requests
import json
url = "https://api.example.com/v1/users"
data = {
"name": "Jane Doe",
"email": "jane@example.com"
}
response = requests.post(
url,
data=json.dumps(data),
headers={"Content-Type": "application/json"}
)
When to use this: Only when you need custom JSON serialization (for example, custom date formatting or decimal handling). In all other cases, use json=data.
Key Difference: `json` vs `data`
| Parameter | Serialization | Content-Type | Use Case |
|---|---|---|---|
json=data |
Automatic (json.dumps) |
Set automatically | Standard JSON APIs |
data=json.dumps(data) |
Manual | Must set manually | Custom serialization |
data=data |
Form-encoded | application/x-www-form-urlencoded |
HTML forms, legacy APIs |
A common mistake is using data=data (without json.dumps) for a JSON API. This sends form-encoded data, not JSON, and the server will likely reject it.
Adding Headers
Standard Headers
headers = {
"Content-Type": "application/json", # Set automatically with json= param
"Accept": "application/json",
"User-Agent": "MyApp/1.0"
}
response = requests.post(url, json=data, headers=headers)
Note that when using json=data, you do not need to set Content-Type manually. The requests library handles it. However, setting Accept and User-Agent is good practice.
API Key Authentication
headers = {
"X-API-Key": "your-api-key-here",
"Accept": "application/json"
}
response = requests.post(
"https://api.example.com/v1/generate",
json={"prompt": "A sunset over mountains", "model": "flux"},
headers=headers
)
Bearer Token Authentication
headers = {
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIs...",
"Accept": "application/json"
}
response = requests.post(url, json=data, headers=headers)
Basic Authentication
from requests.auth import HTTPBasicAuth
response = requests.post(
url,
json=data,
auth=HTTPBasicAuth("username", "password")
)
# Shorthand
response = requests.post(url, json=data, auth=("username", "password"))
Handling Responses
Parsing JSON Responses
response = requests.post(url, json=data)
# Check if the request was successful
if response.ok: # True for status codes 200-299
result = response.json()
print(result)
else:
print(f"Error: {response.status_code}")
print(response.text)
Complete Response Inspection
response = requests.post(url, json=data)
print(f"Status: {response.status_code}")
print(f"Headers: {dict(response.headers)}")
print(f"Content-Type: {response.headers.get('Content-Type')}")
print(f"Response time: {response.elapsed.total_seconds()}s")
print(f"Body: {response.text}")
# Parse JSON only if Content-Type is JSON
if "application/json" in response.headers.get("Content-Type", ""):
print(f"JSON: {response.json()}")
Handling Different Status Codes
response = requests.post(url, json=data)
match response.status_code:
case 200 | 201:
result = response.json()
print(f"Success: {result}")
case 400:
print(f"Bad request: {response.json().get('error', 'Unknown')}")
case 401:
print("Unauthorized: check your API key")
case 403:
print("Forbidden: insufficient permissions")
case 404:
print("Endpoint not found: check your URL")
case 429:
retry_after = response.headers.get("Retry-After", "60")
print(f"Rate limited: retry after {retry_after} seconds")
case 500:
print("Server error: try again later")
case _:
print(f"Unexpected status: {response.status_code}")
Error Handling and Retries
Basic Error Handling
import requests
from requests.exceptions import (
ConnectionError,
Timeout,
HTTPError,
RequestException
)
try:
response = requests.post(
url,
json=data,
timeout=30 # Always set a timeout
)
response.raise_for_status() # Raises HTTPError for 4xx/5xx
result = response.json()
except ConnectionError:
print("Failed to connect to server")
except Timeout:
print("Request timed out")
except HTTPError as e:
print(f"HTTP error: {e.response.status_code}")
except RequestException as e:
print(f"Request failed: {e}")
Retry with Exponential Backoff
import requests
import time
def post_with_retry(url, json_data, headers=None, max_retries=3, base_delay=1):
"""Send a POST request with exponential backoff retry."""
for attempt in range(max_retries):
try:
response = requests.post(
url,
json=json_data,
headers=headers,
timeout=30
)
# Don't retry client errors (except 429)
if 400 <= response.status_code < 500 and response.status_code != 429:
response.raise_for_status()
# Retry on rate limit
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", base_delay * (2 ** attempt)))
print(f"Rate limited. Waiting {retry_after}s...")
time.sleep(retry_after)
continue
# Retry on server errors
if response.status_code >= 500:
raise requests.exceptions.HTTPError(response=response)
return response
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
if attempt == max_retries - 1:
raise
delay = base_delay * (2 ** attempt)
print(f"Attempt {attempt + 1} failed: {e}. Retrying in {delay}s...")
time.sleep(delay)
raise Exception(f"All {max_retries} attempts failed")
# Usage
response = post_with_retry(
"https://api.example.com/v1/data",
json_data={"key": "value"},
headers={"X-API-Key": "your-key"}
)
Using `requests.Session` for Connection Reuse
When making multiple requests to the same server, use a Session to reuse TCP connections:
import requests
session = requests.Session()
session.headers.update({
"X-API-Key": "your-api-key",
"Accept": "application/json"
})
# All requests through this session share the same headers and connection pool
response1 = session.post("https://api.example.com/v1/users", json={"name": "Alice"})
response2 = session.post("https://api.example.com/v1/users", json={"name": "Bob"})
response3 = session.post("https://api.example.com/v1/users", json={"name": "Charlie"})
session.close()
Sessions also handle cookies, connection pooling, and keep-alive automatically.
Advanced Patterns
Sending Nested JSON
data = {
"user": {
"name": "Jane Doe",
"address": {
"street": "123 Main St",
"city": "San Francisco",
"state": "CA"
}
},
"preferences": {
"notifications": True,
"theme": "dark",
"languages": ["en", "es", "fr"]
}
}
response = requests.post(url, json=data)
Sending JSON with File Uploads
You cannot use json= and files= in the same request. For APIs that expect both, encode the JSON as a form field:
import json
files = {
"image": ("photo.jpg", open("photo.jpg", "rb"), "image/jpeg"),
"metadata": (None, json.dumps({"title": "My Photo", "tags": ["nature"]}), "application/json")
}
response = requests.post(url, files=files, headers={"X-API-Key": "key"})
Streaming Large JSON Responses
For APIs that return large responses:
response = requests.post(url, json=data, stream=True)
# Process the response in chunks
for chunk in response.iter_content(chunk_size=8192):
process_chunk(chunk)
Async Requests with httpx
For applications that need async support, use httpx (which has a requests-compatible API):
import httpx
import asyncio
async def post_json_async():
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.example.com/v1/data",
json={"key": "value"},
headers={"X-API-Key": "your-key"},
timeout=30.0
)
return response.json()
# Run it
result = asyncio.run(post_json_async())
Parallel Requests with asyncio
import httpx
import asyncio
async def send_batch(items):
async with httpx.AsyncClient() as client:
tasks = [
client.post(
"https://api.example.com/v1/process",
json={"item": item},
headers={"X-API-Key": "your-key"}
)
for item in items
]
responses = await asyncio.gather(*tasks)
return [r.json() for r in responses]
items = ["item1", "item2", "item3", "item4", "item5"]
results = asyncio.run(send_batch(items))
Common Mistakes and Fixes
| Mistake | Problem | Fix |
|---|---|---|
data=data instead of json=data |
Sends form-encoded, not JSON | Use json=data |
Missing timeout parameter |
Request hangs indefinitely | Always set timeout=30 |
Not calling response.raise_for_status() |
Silent failures on 4xx/5xx | Add error checking |
| Hardcoding API keys | Security risk | Use environment variables |
| Not using a Session | Slow for multiple requests | Use requests.Session() |
| Ignoring Content-Type response header | Crashes on non-JSON responses | Check before calling .json() |
Quick Reference
import requests
# Simple POST
response = requests.post("https://api.example.com/data", json={"key": "value"})
# With headers
response = requests.post(url, json=data, headers={"X-API-Key": "key"})
# With timeout
response = requests.post(url, json=data, timeout=30)
# With auth
response = requests.post(url, json=data, auth=("user", "pass"))
# Check response
response.status_code # 200
response.json() # Parsed JSON
response.text # Raw text
response.headers # Response headers
response.ok # True if 200-299
response.raise_for_status() # Raises on 4xx/5xx
Conclusion
Sending JSON POST requests with Python requests is straightforward once you know the patterns. Use json=data for automatic serialization, always set timeouts, implement retry logic for production code, and use Sessions when making multiple requests to the same server.
If you are looking for a practical API to test your Python HTTP skills, Hypereal AI provides a REST API for AI-powered image generation, video creation, and voice synthesis. The endpoints accept standard JSON POST requests with API key authentication -- a perfect real-world use case for the patterns covered in this guide.
Related Articles
Start Building Today
Get 35 free credits on signup. No credit card required. Generate your first image in under 5 minutes.
