ScreenshotNeo

BlogHow-to

How to Create Screenshots of Multiple URLs with the Thumbalizr API

Thumbalizr’s Embed API documents one target URL per request. Learn to sign and run one request per URL, track each result, and handle failures.

By the ScreenshotNeo team4 October 202610 min read

Short answer: Thumbalizr’s published Embed API documents a single url parameter per request, not a multi-URL batch endpoint. To capture several pages, loop over your URL list and make one signed request per URL. Record each response independently: QUEUED means processing is still underway, OK means the screenshot is ready, and FAILED means that capture failed. See the official Embed API documentation for the endpoint and parameters.

How the multi-URL workflow works

  1. Get an Embed API key and secret from your Thumbalizr member account. Keep the secret in server-side configuration; do not put it in public JavaScript or HTML.
  2. For each URL, construct the encoded query string, including the capture options you want.
  3. Calculate the token as the MD5 hex digest of the exact query string followed by your secret: md5(query + secret).
  4. Request https://api.thumbalizr.com/api/v1/embed/EMBED_API_KEY/TOKEN/?QUERY.
  5. Save the response and status for that URL. Continue with the remaining URLs even if one fails.

The token depends on the query string. Encode reserved characters in the target URL and use the same query bytes both to calculate the token and to make the request. The API documentation specifically warns that parameters, especially url, must be encoded correctly.

Runnable Python example

This Python 3 example uses only the standard library. Set THUMBALIZR_KEY and THUMBALIZR_SECRET in the environment, edit URLS, then run the script. It writes completed image responses to files and records a status for each input. It uses urlencode to build one canonical query string for both signing and the request.

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

API_KEY = os.environ["THUMBALIZR_KEY"]
SECRET = os.environ["THUMBALIZR_SECRET"]
URLS = [
    "https://example.com/",
    "https://example.org/search?q=thumbalizr&page=2",
]
OPTIONS = {"mode": "page", "format": "png"}
OUTPUT_DIR = Path("thumbalizr-shots")
OUTPUT_DIR.mkdir(exist_ok=True)


def capture(url: str, index: int) -> dict:
    params = {"url": url, **OPTIONS}
    query = urlencode(params)
    token = hashlib.md5((query + SECRET).encode("utf-8")).hexdigest()
    endpoint = f"https://api.thumbalizr.com/api/v1/embed/{API_KEY}/{token}/?{query}"
    request = Request(endpoint, headers={"User-Agent": "thumbalizr-url-batch/1.0"})

    try:
        with urlopen(request, timeout=90) as response:
            status = response.headers.get("X-Thumbalizr-Status", "UNKNOWN")
            generated = response.headers.get("X-Thumbalizr-Generated")
            error = response.headers.get("X-Thumbalizr-Error")
            body = response.read()
            result = {"url": url, "http_status": response.status,
                      "status": status, "generated": generated, "error": error}
            if status == "OK":
                suffix = "jpg" if OPTIONS.get("format") == "jpg" else "png"
                path = OUTPUT_DIR / f"shot-{index}.{suffix}"
                path.write_bytes(body)
                result["file"] = str(path)
            elif status == "QUEUED":
                result["note"] = "Processing is not complete; handle this response according to your account/API workflow."
            return result
    except HTTPError as exc:
        return {"url": url, "http_status": exc.code, "status": "HTTP_ERROR",
                "error": exc.reason}
    except (URLError, TimeoutError) as exc:
        return {"url": url, "status": "TRANSPORT_ERROR", "error": str(exc)}


for i, url in enumerate(URLS, start=1):
    result = capture(url, i)
    print(result)
    # A small pause limits how quickly this client submits requests.
    time.sleep(0.2)

Use values and options supported by your account. The docs list width, format, quality, timestamp, size, delay, bwidth, bheight, and country. Some are plan dependent; details appear below. The documentation’s token recipe is specific: do not reorder, normalize, or separately encode the query after signing.

Runnable Node.js example

This example uses Node.js with built-in crypto and fetch (Node 18 or newer). Set the two environment variables and run the file. It handles each target separately and writes a returned image only when the status is OK.

import { createHash } from 'node:crypto';
import { writeFile, mkdir } from 'node:fs/promises';

const apiKey = process.env.THUMBALIZR_KEY;
const secret = process.env.THUMBALIZR_SECRET;
if (!apiKey || !secret) throw new Error('Set THUMBALIZR_KEY and THUMBALIZR_SECRET');

const urls = [
  'https://example.com/',
  'https://example.org/search?q=thumbalizr&page=2',
];
const options = { mode: 'page', format: 'png' };
await mkdir('thumbalizr-shots', { recursive: true });

async function capture(url, index) {
  const params = new URLSearchParams({ url, ...options });
  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}`;

  try {
    const response = await fetch(endpoint, { signal: AbortSignal.timeout(90000) });
    const status = response.headers.get('X-Thumbalizr-Status') ?? 'UNKNOWN';
    const result = {
      url,
      httpStatus: response.status,
      status,
      generated: response.headers.get('X-Thumbalizr-Generated'),
      error: response.headers.get('X-Thumbalizr-Error'),
    };
    const body = Buffer.from(await response.arrayBuffer());
    if (status === 'OK') {
      const ext = options.format === 'jpg' ? 'jpg' : 'png';
      const file = `thumbalizr-shots/shot-${index}.${ext}`;
      await writeFile(file, body);
      result.file = file;
    } else if (status === 'QUEUED') {
      result.note = 'Processing is not complete; handle this response according to your account/API workflow.';
    }
    return result;
  } catch (error) {
    return { url, status: 'TRANSPORT_ERROR', error: String(error) };
  }
}

for (let i = 0; i < urls.length; i++) {
  console.log(await capture(urls[i], i + 1));
  await new Promise(resolve => setTimeout(resolve, 200));
}

One request with cURL

Because the token covers the exact encoded query plus the secret, the easiest way to construct a correct cURL request is to generate the token first. This example uses a simple URL with no reserved query characters; for arbitrary URLs, use the Python or Node.js encoder above so the signed query and sent query stay identical.

export THUMBALIZR_KEY='YOUR_EMBED_API_KEY'
export THUMBALIZR_SECRET='YOUR_SECRET'
QUERY='url=https%3A%2F%2Fexample.com%2F&mode=page&format=png'
TOKEN="$(printf '%s' "${QUERY}${THUMBALIZR_SECRET}" | openssl dgst -md5 | awk '{print $NF}')"
curl --fail-with-body --silent --show-error \
  "https://api.thumbalizr.com/api/v1/embed/${THUMBALIZR_KEY}/${TOKEN}/?${QUERY}" \
  -D response-headers.txt -o screenshot.png

For multiple URLs, repeat the signing and request steps once per URL. Do not reuse one token for another target or a changed option set. Inspect response-headers.txt for X-Thumbalizr-Status, X-Thumbalizr-Generated, and X-Thumbalizr-Error. The cURL example is a single request; a loop still needs to encode and sign each URL independently.

Parameters and plan-dependent behavior

Only url is required by the Embed API; unspecified settings can use your profile defaults. The documentation lists these query options:

Parameter What it controls Documented behavior and caveats
url Target page Required. Encode it as a query parameter, including its own ?, &, spaces, and non-ASCII characters.
width Output thumbnail width Documented range is 1–2,000; the available range varies by tier.
format Thumbnail format Documented values: jpg and png.
quality JPEG quality Range 10–100; applies to JPEG thumbnails.
timestamp Request a newly generated thumbnail Use a different value to request a new image; documentation gives a date/time string as an example. Not available on the free tier shown in the docs.
size Capture area page for full page or screen for visible screen. Availability depends on plan.
delay Wait after page load Documented range 1–30 seconds; allowed values and defaults depend on plan.
bwidth, bheight Browser viewport dimensions Documentation lists width up to 2,000 and height up to 1,600, with tier-specific limits.
country Browser location Documentation names us and germany; availability is plan dependent.

Confirm the current limits and options for your account on Thumbalizr’s Features & Pricing page and demo page before scheduling a large run. The published feature page lists 100 monthly screenshots on Free, 2,000 on Silver, 3,000 on Gold, and 5,000 or more on Platinum. Its demo describes Free as watermarked and screen-size only, Silver as offering no watermark and full-page or screen capture, and Gold/Platinum as adding custom delay and US or European browser location. These displayed plan details can change.

Batch design: failures, retries, and scale

  • Keep results per URL. Store the input URL, request time, HTTP status, Thumbalizr status, error header, and output path. A failure for one page should not erase successful results from other requests.
  • Treat QUEUED as pending. The status header distinguishes queued work from a completed image. Do not mark the job successful merely because the HTTP request returned.
  • Retry selectively. Retry transport errors and transient failures with a capped exponential backoff. Avoid tight retry loops, and do not treat a deterministic bad URL or unsupported option as transient.
  • Make output names deterministic. Use a stable index or a hash of the normalized input URL, and write to a temporary file before renaming it if consumers may read outputs during a run.
  • Control concurrency. Start with a small number of in-flight requests and increase only if the account and observed responses support it. The documentation does not publish a concurrency limit or throughput benchmark.
  • Plan against quota. Count intended captures against the monthly allowance and leave room for reruns. Large jobs can be split into locally managed batches for easier monitoring; this is client-side organization, not a documented Thumbalizr batch endpoint.

For reliable automation, persist a manifest and resume only URLs without an OK result. Do not blindly rerun the entire list after a process interruption. If you intentionally want a fresh thumbnail rather than a cached one, use the documented timestamp option where your plan permits it.

Security and compatibility

The Embed API’s token is an MD5 digest over the query and secret, as documented by Thumbalizr. Keep the secret private and calculate tokens on a trusted server. If you expose a tokenized URL in a public page, remember that the resulting request URL itself contains the target and parameters; avoid sensitive URLs or query values. Follow Thumbalizr’s documented signing scheme exactly for compatibility.

Thumbalizr marks its older API as legacy and recommends the Embed API for new implementations. The legacy API is described as better suited to downloading thumbnails offline when the key can be hidden from visitors. The official legacy API documentation explains that distinction. Thumbalizr’s Python and PHP library examples show URL generation per target; they do not document a multi-URL endpoint.

Troubleshooting

Symptom Likely cause What to do
Token or authentication failure The query used for signing differs from the query sent, or the key/secret is incorrect. Build the query once, hash that exact string plus the secret, and use the same string in the URL. Check credentials and avoid changing parameter order or encoding between those steps.
Capture points to the wrong page or loses query values The target URL’s reserved characters were not encoded as part of the outer API query. Pass the target URL through a query encoder such as Python’s urlencode or Node’s URLSearchParams; do not concatenate a raw URL containing &.
HTTP response arrives but no finished screenshot The Thumbalizr status is QUEUED. Track it as pending, retain the response metadata, and follow the processing behavior documented for your account rather than saving it as a finished image.
Capture marked FAILED The service could not complete that target; the response may include an error header. Log X-Thumbalizr-Error, inspect the target and requested options, and retry only if the cause appears transient.
Requested full page, dimensions, delay, or location is not honored The option is unavailable or restricted on the selected tier, or the request relied on an account default. Check the current plan table and explicitly send supported values.
Image has a watermark or is cropped Plan behavior: the published demo says Free includes a watermark and screen-size capture. Check the current plan comparison and select a tier that supports the required watermark and capture size.
Timeout or connection error The target load or network request exceeded the client timeout or encountered a transient issue. Use a suitable timeout, record the transport failure separately from API status, and retry with backoff rather than immediately flooding the endpoint.
Saved file is not a valid image A non-image response, error body, or queued result was written with an image extension. Save only after checking for OK; retain headers and inspect the response before writing the output artifact.

Performance, reliability, and cost

With one request per URL, total work grows with the number of targets. The documentation does not publish a multi-URL endpoint, request-rate limit, or performance benchmark, so do not assume a fixed batch duration. Use bounded concurrency, per-request timeouts, and a durable result manifest. Increase concurrency gradually while watching statuses and errors.

Budget against the plan’s current monthly screenshot allowance. The published pricing/features page shows Free at 100, Silver at 2,000, Gold at 3,000, and Platinum at 5,000 or more monthly screenshots, but plan prices and allowances can change. Confirm terms in the account and on the current official page before relying on those figures. Per the API’s status model, distinguish a queued result from a completed one and a failed one when reporting job totals.

Or skip the browser setup

ScreenshotNeo provides a screenshot API with one GET request per URL, plus a bulk capture option for up to 100 URLs per call. It removes cookie banners, newsletter popups, and chat widgets 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. Every feature is on every plan. See the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.

FAQ

Can I send an array of URLs to Thumbalizr?

The published Embed API documents one url parameter per request. Send one request for each URL unless Thumbalizr publishes a batch endpoint separately.

Does QUEUED mean the screenshot is ready?

No. It means processing is underway. Track it separately from OK and FAILED.

Can I use a request URL generated in a browser?

The secret should remain server-side. Generate the signed URL on a trusted backend, or use a server-side script for offline batches.

Should I use Thumbalizr’s old API?

For new integrations, Thumbalizr recommends the Embed API. The old API is described as legacy and oriented toward offline thumbnail downloads when credentials can be hidden from visitors.