How to Make API Calls Using Python
Learn how to call REST APIs in Python with Requests or urllib, authenticate safely, parse responses, handle errors, retry reliably, and capture screenshots.
Short answer: an API call is an HTTP request to an endpoint, followed by checking the response status, headers, and body. In Python, use Requests for a concise interface, or the standard-library urllib.request when you cannot add dependencies.
A reliable call follows this sequence:
- Read the API documentation for its endpoint, method, parameters, authentication, and response format.
- Build the URL and encode query parameters correctly.
- Send the request with an explicit timeout.
- Check the HTTP status before trusting the body.
- Parse JSON only when the response is JSON and validate the fields your code needs.
- Handle transport errors, HTTP errors, rate limits, and transient failures according to that API’s policy.
1. Make a GET request with Requests
Install Requests in your project environment:
python -m pip install requests
This complete example reads an API token from an environment variable, sends query parameters with params, applies a timeout, checks for an HTTP error, verifies that the response is JSON, and prints selected fields.
import os
import requests
API_TOKEN = os.environ["API_TOKEN"]
url = "https://api.example.com/v1/items"
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {API_TOKEN}",
}
params = {"limit": 20, "status": "active"}
try:
response = requests.get(
url,
params=params,
headers=headers,
timeout=(5, 30), # connect timeout, read timeout
)
response.raise_for_status()
except requests.exceptions.Timeout:
raise RuntimeError("The API did not respond before the timeout")
except requests.exceptions.ConnectionError as exc:
raise RuntimeError(f"Could not connect to the API: {exc}") from exc
except requests.exceptions.HTTPError as exc:
# Log status and a bounded body in real applications; never log tokens.
raise RuntimeError(
f"API returned HTTP {response.status_code}: {response.text[:500]}"
) from exc
content_type = response.headers.get("content-type", "").lower()
if "application/json" not in content_type:
raise RuntimeError(f"Expected JSON, received {content_type or 'unknown content type'}")
data = response.json()
for item in data.get("items", []):
print(item.get("id"), item.get("name"))
params lets Requests URL-encode values safely. The resulting URL might look like https://api.example.com/v1/items?limit=20&status=active; do not build that string by concatenating unescaped user input.
2. Send JSON with POST, PUT, or PATCH
Use json= for a JSON request body. Requests serializes the Python object and sets the appropriate content type.
import os
import requests
url = "https://api.example.com/v1/items"
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {os.environ['API_TOKEN']}",
}
payload = {"name": "Ada", "active": True}
response = requests.post(url, json=payload, headers=headers, timeout=30)
response.raise_for_status()
created = response.json()
print(created["id"])
Use requests.put or requests.patch in the same way when the API documents those methods. Use data= for form-encoded data, and use files= for multipart uploads.
3. Authentication and request headers
Follow the scheme documented by the API. Common forms include:
| Scheme | Requests example | Security note |
|---|---|---|
| Bearer token | headers={"Authorization": f"Bearer {token}"} |
Keep the token in an environment variable or secret manager. |
| API key header | headers={"X-API-Key": api_key} |
Use the exact header name required by the service. |
| Basic authentication | requests.get(url, auth=(username, password), timeout=30) |
Use HTTPS and do not commit credentials. |
| OAuth | Send the access token as documented by the provider. | Refresh expired tokens according to the provider’s flow. |
Never put secrets in committed source, screenshots, URLs, logs, or exception messages. Leave TLS certificate verification enabled. Disabling verification hides certificate problems and weakens transport security.
4. Reuse connections with a Session
A Session preserves cookies and uses connection pooling, which helps when making multiple calls to the same host.
import os
import requests
with requests.Session() as session:
session.headers.update({
"Accept": "application/json",
"Authorization": f"Bearer {os.environ['API_TOKEN']}",
})
for page in range(1, 4):
response = session.get(
"https://api.example.com/v1/items",
params={"page": page, "limit": 100},
timeout=(5, 30),
)
response.raise_for_status()
print(response.json())
5. Use Python’s standard library with urllib
urllib.request is available without installing a package. It uses Request objects and urlopen; HTTP failures raise HTTPError, while unreachable servers and other network problems commonly raise URLError. Catch HTTPError before URLError because it is a subclass.
import json
import os
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
query = urlencode({"limit": 20, "status": "active"})
request = Request(
f"https://api.example.com/v1/items?{query}",
headers={
"Accept": "application/json",
"Authorization": f"Bearer {os.environ['API_TOKEN']}",
},
method="GET",
)
try:
with urlopen(request, timeout=30) as response:
if "application/json" not in response.headers.get_content_type():
raise RuntimeError("Expected a JSON response")
data = json.load(response)
except HTTPError as exc:
print("HTTP failure", exc.code)
print(exc.read().decode("utf-8", errors="replace")[:500])
except URLError as exc:
print("Network failure", exc.reason)
payload = json.dumps({"name": "Ada", "active": True}).encode("utf-8")
post_request = Request(
"https://api.example.com/v1/items",
data=payload,
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Bearer {os.environ['API_TOKEN']}",
},
method="POST",
)
with urlopen(post_request, timeout=30) as response:
created = json.load(response)
print(created["id"])
6. Parse and validate responses
A successful status code does not guarantee the shape your program expects. Check the content type, parse JSON, and validate required fields before using them.
def require_item(data):
if not isinstance(data, dict):
raise ValueError("Response JSON must be an object")
item_id = data.get("id")
if not isinstance(item_id, (str, int)):
raise ValueError("Response is missing a valid id")
return item_id
try:
item_id = require_item(response.json())
except ValueError as exc:
# Covers malformed JSON and an unexpected response shape.
raise RuntimeError("The API response could not be used") from exc
Do not use response.json() as your success check. A server can return a JSON error document with a 401, 404, or 500 status. Call raise_for_status() first, then parse the expected representation.
7. Handle status codes and retries
| Status | Typical meaning | Action |
|---|---|---|
| 2xx | Request succeeded | Parse and validate the response. |
| 400 | Invalid request | Fix parameters, JSON, or required fields; retries will not help. |
| 401 | Missing or invalid authentication | Check the token, scheme, and expiration. |
| 403 | Authenticated but not allowed | Check permissions, scopes, and account policy. |
| 404 | Unknown endpoint or resource | Check the base URL, API version, and identifier. |
| 409 | Conflict | Resolve the resource conflict; follow API-specific guidance. |
| 429 | Rate limit exceeded | Honor Retry-After when present and reduce request rate. |
| 5xx | Server-side failure | Retry only when the operation is safe and the API allows it. |
Use bounded exponential backoff with jitter for transient connection failures, 429 responses, and selected 5xx responses. Do not blindly retry non-idempotent POST requests: a timeout can occur after the server created the resource. Use an idempotency key when the API supports one.
import random
import time
import requests
RETRYABLE = {429, 500, 502, 503, 504}
def get_with_retry(url, *, params=None, headers=None, attempts=4):
for attempt in range(attempts):
try:
response = requests.get(
url, params=params, headers=headers, timeout=(5, 30)
)
if response.status_code not in RETRYABLE:
response.raise_for_status()
return response
if attempt == attempts - 1:
response.raise_for_status()
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(30, 2 ** attempt)
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
if attempt == attempts - 1:
raise
delay = min(30, 2 ** attempt)
time.sleep(delay + random.uniform(0, 0.25))
response = get_with_retry("https://api.example.com/v1/items")
print(response.json())
8. cURL and Node.js equivalents
cURL is useful for isolating whether a problem is in Python or in the API request itself:
curl --fail-with-body --request GET \
--url 'https://api.example.com/v1/items?limit=20' \
--header "Accept: application/json" \
--header "Authorization: Bearer $API_TOKEN"
Node.js 18 and later include fetch:
const token = process.env.API_TOKEN;
const url = new URL('https://api.example.com/v1/items');
url.searchParams.set('limit', '20');
const res = await fetch(url, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 500)}`);
}
const data = await res.json();
console.log(data);
9. Or skip the browser setup
When the API call you need is a website screenshot, ScreenshotNeo provides a single GET request that returns PNG, JPEG, WebP, or PDF. Its cookie and consent handling removes 60+ known consent platforms, newsletter popups, and chat widgets before capture, and each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the request was billed. The MCP server includes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. See the ScreenshotNeo API documentation for all options.
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo also supports full-page captures with lazy images loaded, CSS element selection, dark mode, device presets, custom viewports, retina scale, PDFs, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, Authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which simplifies migrations.
Free accounts include 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free. Create a free ScreenshotNeo account.
10. Performance, reliability, and cost
- Set both connect and read timeouts. A single total timeout prevents a hung call from consuming a worker indefinitely.
- Use a Session for repeated calls to one host so connections can be reused.
- Paginate deliberately and respect documented rate limits. Avoid unbounded concurrency.
- Retry only transient failures, with a maximum attempt count and jitter.
- Record method, host, path, status, duration, and a provider request ID when available. Redact authorization headers and sensitive query values.
- Bound error-body logging because providers may return credentials or personal data in diagnostic text.
- Cache safe, repeatable GET responses when freshness permits. Do not cache user-specific data without an explicit policy.
- For ScreenshotNeo, cache hits are identified in the response and are not billed; choose a TTL that matches how often the target page changes.
11. Troubleshooting common errors
| Symptom | Likely cause | Fix |
|---|---|---|
ModuleNotFoundError: requests |
Requests is not installed in the active environment. | Run python -m pip install requests using the same interpreter that runs the script. |
| 401 Unauthorized | Missing, expired, or incorrectly formatted credentials. | Check the environment variable, authentication scheme, and required header. |
| 403 Forbidden | The token lacks permission or the account is restricted. | Check scopes, roles, resource ownership, and provider policy. |
| 429 Too Many Requests | Rate limit exceeded. | Slow down, honor Retry-After, and retry with backoff. |
ReadTimeout |
The server or response body took too long. | Use a realistic read timeout, reduce payload size, or use the provider’s asynchronous API. |
ConnectionError or URLError |
DNS, proxy, firewall, TLS, or network failure. | Check DNS and proxy settings, verify the hostname, and keep certificate verification enabled. |
| JSON decode error | The body is HTML, empty, or malformed JSON. | Check the status and Content-Type before parsing; inspect a bounded body excerpt. |
| Works in cURL but not Python | Different headers, proxy, encoding, or URL construction. | Print the prepared URL and safe headers, then compare them with cURL. |
| ScreenshotNeo returns a non-image response | The page verdict indicates a bot check, blank page, timeout, or failed load. | Inspect X-Page-Verdict and X-Billed, then adjust waits, headers, user agent, or target URL. |
12. Practical checklist
- Read the endpoint documentation and confirm method, parameters, authentication, and response type.
- Keep secrets outside source control.
- Use
params=andjson=instead of hand-built strings. - Set explicit connect and read timeouts.
- Call
raise_for_status()before parsing a successful response. - Validate required fields and content types.
- Handle 401, 403, 404, 409, 429, and 5xx responses separately where useful.
- Retry only safe transient operations with bounded backoff.
- Redact tokens and sensitive response data from logs.
- Use a Session for repeated Requests calls.
13. FAQ
Is Requests better than urllib?
Requests is usually easier to read and provides convenient parameters, JSON handling, sessions, pooling, and authentication helpers. urllib is built into Python and is suitable when minimizing dependencies matters.
Should I use GET or POST?
Use the method defined by the API. GET commonly retrieves a representation; POST commonly submits data or creates a resource. Do not change methods based only on convenience.
Why did my API call return status 200 but still fail?
The body may contain an application-level error or an unexpected schema. Parse only the documented content type and validate the fields your code requires.
How should I store an API key locally?
Use an environment variable or a secret manager, restrict access to the secret, and exclude local secret files from version control.
When should I choose an asynchronous API?
Choose one when a provider documents long-running work, large batches, or webhook completion. Keep a normal request timeout for the job-creation call and poll or receive the signed callback as documented.


