ScreenshotNeo

BlogHow-to

Thumbalizr API Tutorial: Capture a Webpage from Python

Build a signed Thumbalizr Embed API URL in Python, configure capture options, and check whether a screenshot is queued, complete, or failed.

By the ScreenshotNeo team4 October 20269 min read

To capture a webpage with Thumbalizr from Python, create an Embed API URL: URL-encode the query parameters, calculate the documented MD5 token from the exact encoded query string followed by your API secret, then request the URL. The response headers report whether the capture is queued, complete, or failed. You need a Thumbalizr account to obtain an Embed API key and secret. Thumbalizr’s API documentation describes this signing method and warns that parameters, especially the target URL, must be encoded correctly.

1. Get your credentials and understand the request

Sign up for a Thumbalizr account and find the Embed API key and secret in the member area. The documented endpoint has this shape:

https://api.thumbalizr.com/api/v1/embed/EMBED_API_KEY/TOKEN/?QUERY_STRING

The url parameter is required. Other options may default to the settings in your account profile. The token is the hexadecimal MD5 digest of the encoded query string concatenated directly with the secret:

token = md5(encoded_query_string + secret)

Keep the key and secret outside source control. Store them in environment variables or a secrets manager. The query string used to calculate the token must be byte-for-byte the same query string sent in the request, including parameter order and encoding. Do not sign one representation and let a separate URL encoder produce another.

2. Build and request a signed URL in Python

This Python 3 example uses only the standard library. It constructs the query once, signs that exact string, and requests the resulting URL. Set THUMBALIZR_API_KEY and THUMBALIZR_API_SECRET in your environment before running it.

import hashlib
import os
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError

API_KEY = os.environ["THUMBALIZR_API_KEY"]
API_SECRET = os.environ["THUMBALIZR_API_SECRET"]

# Supply an absolute, publicly reachable URL.
params = {
    "url": "https://example.com/",
    "width": 1280,
    "format": "png",
    "size": "page",
    "delay": 5,
}

# Encode exactly once, then use these same bytes for signing and requesting.
query = urlencode(params)
token = hashlib.md5((query + API_SECRET).encode("utf-8")).hexdigest()
request_url = (
    f"https://api.thumbalizr.com/api/v1/embed/"
    f"{API_KEY}/{token}/?{query}"
)

request = Request(request_url, headers={"User-Agent": "thumbalizr-python-example/1.0"})
try:
    with urlopen(request, timeout=90) as response:
        image_bytes = response.read()
        status = response.headers.get("X-Thumbalizr-Status")
        generated = response.headers.get("X-Thumbalizr-Generated")
        error = response.headers.get("X-Thumbalizr-Error")
        content_type = response.headers.get("Content-Type")

    print("HTTP response received")
    print("Thumbalizr status:", status)
    print("Generated:", generated)
    print("Error:", error)
    print("Content type:", content_type)

    if status == "OK":
        extension = "png" if params["format"] == "png" else "jpg"
        with open(f"capture.{extension}", "wb") as output:
            output.write(image_bytes)
        print(f"Saved capture.{extension} ({len(image_bytes)} bytes)")
    elif status == "QUEUED":
        print("Capture is still processing; follow the API's documented retrieval flow.")
    elif status == "FAILED":
        raise RuntimeError(f"Thumbalizr capture failed: {error or 'no error detail'}")
    else:
        print("The response did not include a recognized capture status.")

except HTTPError as exc:
    print("HTTP error:", exc.code, exc.reason)
    print("Response headers:", dict(exc.headers.items()))
    raise
except URLError as exc:
    raise RuntimeError(f"Could not reach Thumbalizr: {exc.reason}") from exc

The example adapts the vendor’s documented algorithm to Python 3 syntax. Thumbalizr’s published Python sample uses Python 2-era syntax; the available documentation does not establish a current Python-version compatibility matrix.

cURL equivalent

For cURL, first generate the same encoded query and MD5 token. This short Python command prints the signed URL without making the request:

export THUMBALIZR_API_KEY="your-key"
export THUMBALIZR_API_SECRET="your-secret"
SIGNED_URL="$(python3 -c 'import hashlib, os; from urllib.parse import urlencode; q=urlencode({"url":"https://example.com/","width":1280,"format":"png","size":"page","delay":5}); t=hashlib.md5((q+os.environ["THUMBALIZR_API_SECRET"]).encode()).hexdigest(); print(f"https://api.thumbalizr.com/api/v1/embed/{os.environ[\"THUMBALIZR_API_KEY\"]}/{t}/?{q}")')"
curl --fail --show-error --dump-header headers.txt "$SIGNED_URL" --output capture.png
cat headers.txt

Check headers.txt for X-Thumbalizr-Status, X-Thumbalizr-Error, and X-Thumbalizr-Generated. This command saves the response body even if the API reports a queued or failed capture, so inspect the status before treating the file as an image.

Node.js equivalent

The same exact-query rule applies in Node.js. This example uses built-in modules, requests the image, checks the status header, and saves the body only when the capture status is OK.

import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";

const apiKey = process.env.THUMBALIZR_API_KEY;
const secret = process.env.THUMBALIZR_API_SECRET;
if (!apiKey || !secret) throw new Error("Set THUMBALIZR_API_KEY and THUMBALIZR_API_SECRET");

const params = new URLSearchParams({
  url: "https://example.com/",
  width: "1280",
  format: "png",
  size: "page",
  delay: "5",
});
const query = params.toString();
const token = createHash("md5").update(query + secret, "utf8").digest("hex");
const endpoint = `https://api.thumbalizr.com/api/v1/embed/${apiKey}/${token}/?${query}`;

const response = await fetch(endpoint, { signal: AbortSignal.timeout(90_000) });
const status = response.headers.get("x-thumbalizr-status");
const error = response.headers.get("x-thumbalizr-error");
const generated = response.headers.get("x-thumbalizr-generated");
const body = Buffer.from(await response.arrayBuffer());

console.log({ httpStatus: response.status, status, error, generated });
if (status === "OK") {
  await writeFile("capture.png", body);
} else if (status === "FAILED") {
  throw new Error(error || "Thumbalizr capture failed");
} else if (status === "QUEUED") {
  console.log("Capture is queued; follow the documented retrieval flow.");
} else {
  throw new Error(`Unexpected response status: ${status}`);
}

3. Choose capture options

Thumbalizr documents the following Embed API options. Defaults and maximums can depend on the account tier or member profile, so confirm the current API documentation and your plan before relying on a particular setting.

Parameter Purpose and considerations
url Required target page URL. Encode it as part of the query string; reserved characters such as &, #, and ? must be encoded correctly.
width Thumbnail width. The documentation displays maxima of 1,280 px for Free and Silver, 1,600 px for Gold, and 2,000 px for Platinum.
format jpg or png. Use PNG when lossless output matters; JPEG quality applies to JPEG thumbnails.
quality JPEG quality setting. It has no stated role for PNG output.
size page for a page-sized capture or screen for a viewport capture. The displayed Free plan default is screen.
delay Wait before capture, in the documented range of 1–30 seconds. The displayed default for Free and Silver is 5 seconds.
bwidth, bheight Browser viewport width and height. These affect the page layout and therefore the captured result.
country The docs list US and Germany, and say to contact Thumbalizr about other countries.
timestamp Requests a new thumbnail rather than relying on an existing one. The documentation says this is unavailable on the displayed Free tier.

Only pass options you need. Extra parameters change the signed query string, so build the final parameter dictionary before encoding and hashing it. Do not append or reorder parameters after calculating the token.

4. Read processing status and save the result safely

The documented status header is X-Thumbalizr-Status. Its values are QUEUED, OK, and FAILED. X-Thumbalizr-Error explains a failure, and X-Thumbalizr-Generated gives the generated timestamp.

  • OK: treat the response as a completed capture and save it with an extension matching the requested format.
  • QUEUED: processing is not complete. Follow the API’s documented retrieval or retry flow; do not mistake the response body for the final image.
  • FAILED: inspect X-Thumbalizr-Error and correct the target or request before retrying.

Also check the HTTP status and content type. A successful HTTP connection does not by itself prove that the body is a finished image. Avoid logging the full signed URL in public logs: it includes the API key and a token derived from the secret.

5. Optional Python package and Django integration

Thumbalizr documents a Python package installable with pip install thumbalizr, with a client constructed from the key and secret and methods for generating a URL and waiting for a download. It also documents a Django extension that uses settings for API_KEY, SECRET, and optional parameters, then exposes a template filter. The research available for this article does not establish current package maintenance or compatibility with current Python and Django versions. For a production integration, check the package release history and source before adding it as a dependency; the standard-library example above makes the signing behavior visible and avoids relying on undocumented compatibility.

6. Troubleshooting

Symptom Likely cause What to do
Authentication or token error The query used for signing differs from the query sent, or the key/secret is wrong. Build the query once, sign that exact string plus the secret, check credentials, and ensure no proxy or later code rewrites the URL.
Target URL containing & or # fails The target URL was inserted without proper query encoding. A fragment marker can also be interpreted as part of the outer request URL. Pass the target as the url value to urlencode; do not hand-concatenate query components.
Status is QUEUED The capture has not completed yet. Use the documented retrieval flow and allow processing time. Do not save the queued response as a final image.
Status is FAILED The target could not be captured or an option was invalid. Read X-Thumbalizr-Error, verify the URL is reachable, then check option values and tier limits.
Capture is too small or shows only the initial viewport width, size, or browser viewport settings do not match the intended output. Choose size=page when a page-sized capture is wanted, and set width and viewport dimensions deliberately.
Dynamic content is missing The page may need more time to render client-side content or load images. Adjust the documented delay within its allowed range, then verify the target’s behavior. A delay cannot guarantee that content requiring interaction will appear.
Python reports a syntax or import error in a vendor sample The published sample uses Python 2-era syntax. Use Python 3 adaptations such as urllib.parse.urlencode and encode the signed string as UTF-8 before hashing.
HTTP succeeds but the saved file is invalid The response may represent a queued/failed result or an error body rather than the requested image. Inspect status, error, HTTP status, and content type before saving or serving the bytes as an image.

7. Performance, reliability, and cost

Capture time depends on the target page and service processing; the cited documentation does not provide a performance benchmark. A longer delay can give client-rendered pages more time, but adds waiting and does not resolve pages that need user interaction. Set a client timeout appropriate to your job, handle QUEUED as a separate state, and avoid rapid repeated requests when a capture is still processing.

The plan page accessed for this research displays 100 screenshots per month on Free, 2,000 on Silver, 3,000 on Gold, and 5,000 or more on Platinum. It displays Silver at €8/$9 monthly, Gold at €12/$13, and Platinum at €18/$20 for 5,000. These are vendor-displayed figures, not independent measurements, and can change; check the current Thumbalizr pricing page before choosing a tier. The docs also show tier-specific width ceilings and indicate that timestamp is unavailable on the displayed Free tier.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF, with options for full-page capture, CSS selectors, viewport and device presets, dark mode, custom CSS and JavaScript, cookies, headers, waits, caching, and more. See the ScreenshotNeo API documentation.

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com/"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up free for ScreenshotNeo and get 1,000 screenshots a month with no card.

FAQ

Does the Thumbalizr token use the raw target URL?

No. The documented token is based on the encoded query string followed by the secret. Encode the target as the url parameter and sign the exact query string you send.

Can I use the vendor’s Python example unchanged on Python 3?

The published sample uses Python 2-era syntax. Use a Python 3 adaptation and verify package compatibility independently if you choose the client library.

What does X-Thumbalizr-Generated contain?

The API documentation describes it as the generated timestamp for the thumbnail.

Is every Thumbalizr account already on the announced new backend?

A Thumbalizr post in April 2026 described a phased transition to ScreenshotCenter. It said existing integrations and settings would continue to work, but the announcement does not confirm that migration is complete for every account or that every planned feature is live. Check current vendor notices for account-specific status.