ScreenshotNeo

BlogHow-to

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.

By the ScreenshotNeo team1 October 20269 min read

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:

  1. Read the API documentation for its endpoint, method, parameters, authentication, and response format.
  2. Build the URL and encode query parameters correctly.
  3. Send the request with an explicit timeout.
  4. Check the HTTP status before trusting the body.
  5. Parse JSON only when the response is JSON and validate the fields your code needs.
  6. 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= and json= 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.