ScreenshotNeo

BlogHow-to

How to Capture Screenshots of Multiple URLs with ApiFlash

Capture a list of URLs with ApiFlash by sending one request per page. Learn sequential and concurrent workflows, rate limiting, and per-URL error handling.

By the ScreenshotNeo team4 October 20268 min read

ApiFlash captures multiple URLs by making one authenticated request to https://api.apiflash.com/v1/urltoimage for each URL. You can process those requests sequentially or concurrently; either way, track each response against its source URL, handle failures individually, and stay within ApiFlash’s documented rate limit of 20 requests per second with a burst size of 400. The examples below save one image per URL.

ApiFlash’s API accepts GET query parameters or POST form data. A target URL must include http:// or https://. Use an HTTP client or form encoder to encode parameters rather than assembling arbitrary URLs by hand. See the official ApiFlash API documentation for current parameter and plan details.

1. Prepare the URL list and API key

Get an access key from your ApiFlash account dashboard. Keep it in an environment variable on a trusted server or in a local shell environment. Do not ship it in browser JavaScript or a public mobile app: users could extract it and consume your quota.

export APIFLASH_ACCESS_KEY="YOUR_API_KEY"

For a production endpoint used by other people, proxy requests through your server, restrict which target URLs it accepts, and rate-limit that endpoint. ApiFlash’s official guides include an Nginx proxy example.

2. Capture multiple URLs with cURL

This shell example processes URLs sequentially and writes each image to a filename derived from its position in the list. It checks the HTTP status so a failed capture is reported instead of silently being treated as an image.

#!/usr/bin/env bash
set -u

: "${APIFLASH_ACCESS_KEY:?Set APIFLASH_ACCESS_KEY first}"
urls=(
  "https://example.com/"
  "https://www.wikipedia.org/"
)

mkdir -p screenshots

for i in "${!urls[@]}"; do
  url="${urls[$i]}"
  output=$(printf 'screenshots/%03d.jpg' "$((i + 1))")
  status=$(curl -sS -G \
    --output "$output" \
    --write-out '%{http_code}' \
    --data-urlencode "access_key=$APIFLASH_ACCESS_KEY" \
    --data-urlencode "url=$url" \
    "https://api.apiflash.com/v1/urltoimage") || status="curl-error"

  if [[ "$status" == "200" ]]; then
    printf 'Saved %s from %s\n' "$output" "$url"
  else
    rm -f "$output"
    printf 'Failed (%s): %s\n' "$status" "$url" >&2
  fi
done

The default response is image data, and the documented default format is JPEG. The script uses indexed filenames to avoid collisions when URLs have the same path or hostname. For a retryable production workflow, also write a manifest containing each input URL, output path, status, and error.

3. Capture multiple URLs with Python

This complete Python script uses a bounded worker pool. It submits at most four requests at once, reports each URL independently, and saves successful JPEG responses. Install the dependency with python -m pip install requests.

import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

import requests

API_KEY = os.environ["APIFLASH_ACCESS_KEY"]
ENDPOINT = "https://api.apiflash.com/v1/urltoimage"
URLS = [
    "https://example.com/",
    "https://www.wikipedia.org/",
]
OUTPUT_DIR = Path("screenshots")
WORKERS = 4

OUTPUT_DIR.mkdir(parents=True, exist_ok=True)


def capture(index, url):
    output = OUTPUT_DIR / f"{index:03d}.jpg"
    try:
        response = requests.get(
            ENDPOINT,
            params={"access_key": API_KEY, "url": url},
            timeout=(10, 90),
        )
        response.raise_for_status()
        content_type = response.headers.get("Content-Type", "")
        if not content_type.startswith("image/"):
            raise ValueError(f"Expected image response, got {content_type!r}")
        output.write_bytes(response.content)
        return {"url": url, "output": str(output), "status": "ok"}
    except (requests.RequestException, OSError, ValueError) as exc:
        output.unlink(missing_ok=True)
        return {"url": url, "status": "error", "error": str(exc)}


with ThreadPoolExecutor(max_workers=WORKERS) as pool:
    futures = [pool.submit(capture, i, url) for i, url in enumerate(URLS, 1)]
    for future in as_completed(futures):
        print(future.result())

Four workers bound simultaneous in-flight requests; they do not guarantee a particular request-start rate if responses finish very quickly. For large batches, add a rate limiter or pacing between request starts so the workflow stays at or below the documented 20 requests per second. The burst allowance is not a target for sustained traffic.

4. Capture multiple URLs with Node.js

This Node.js example uses built-in fetch and a small worker pool. Save it as capture.mjs and run node capture.mjs with APIFLASH_ACCESS_KEY set. It requires a Node.js version with global fetch.

import { mkdir, writeFile, rm } from "node:fs/promises";

const accessKey = process.env.APIFLASH_ACCESS_KEY;
if (!accessKey) throw new Error("Set APIFLASH_ACCESS_KEY first");

const endpoint = "https://api.apiflash.com/v1/urltoimage";
const urls = ["https://example.com/", "https://www.wikipedia.org/"];
const outputDir = "screenshots";
const workerCount = 4;
await mkdir(outputDir, { recursive: true });

async function capture(index, url) {
  const output = `${outputDir}/${String(index).padStart(3, "0")}.jpg`;
  const params = new URLSearchParams({ access_key: accessKey, url });
  try {
    const response = await fetch(`${endpoint}?${params}`, {
      signal: AbortSignal.timeout(90000),
    });
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${await response.text()}`);
    }
    const contentType = response.headers.get("content-type") ?? "";
    if (!contentType.startsWith("image/")) {
      throw new Error(`Expected image response, got ${contentType}`);
    }
    await writeFile(output, Buffer.from(await response.arrayBuffer()));
    return { url, output, status: "ok" };
  } catch (error) {
    await rm(output, { force: true });
    return { url, status: "error", error: String(error) };
  }
}

let next = 0;
async function worker() {
  while (next < urls.length) {
    const index = next++;
    console.log(await capture(index + 1, urls[index]));
  }
}
await Promise.all(Array.from({ length: workerCount }, worker));

As with the Python pool, cap concurrency and add request-start throttling for high-volume lists. If the job can be interrupted, persist the URL list and per-URL result state so completed captures do not need to be repeated.

5. Choose sequential or concurrent requests

Approach Use it when Trade-off
Sequential The list is short, or simplicity and predictable request pacing matter most. Elapsed time grows with every capture.
Bounded concurrency The list is larger and requests can be handled independently. Requires per-request result tracking, error handling, and rate control.

ApiFlash’s concurrency guide says concurrent requests are allowed subject to the request rate. The guide was last updated January 25, 2023, so check the current guide and API reference before relying on operational limits. A worker count is a limit on simultaneous work, not a precise requests-per-second limiter. If you need a strict ceiling, throttle starts with a token bucket or a paced queue.

6. Select response type and capture options

With the default direct image response, save the response bytes as an image. If your workflow needs a screenshot link or extracted page content, use response_type=json; the documentation describes JSON output that can include a screenshot link and optional HTML or text extraction. Parse the JSON response instead of saving it with an image extension.

The documented defaults are JPEG output, a 1920 × 1080 viewport, a viewport screenshot rather than a full-page capture, and direct image data. Relevant documented options include:

  • Image format and quality: PNG and WebP are also supported; quality applies to supported lossy output.
  • Page extent and viewport: full-page capture, custom viewport dimensions, and scrolling behavior.
  • Freshness and caching: request fresh content with fresh or configure cache TTL as documented.
  • Page targeting and readiness: CSS selectors, cookies, headers, user-agent emulation, and wait settings.
  • Locale context: geographic and time-zone settings.
  • Extraction: optional HTML or text when using JSON response mode.

Some options may depend on the account plan. Verify current plan availability in ApiFlash’s documentation. For delayed page rendering, the docs advise considering wait_for or wait_until instead of relying only on a fixed delay. The documented wait_for aborts if its selector does not appear within 15 seconds.

7. Handle errors and retries per URL

Do not mark a whole batch successful because some requests worked. Store one result per input URL, including the HTTP status, a useful error summary, and the eventual output location. ApiFlash documents these common response codes:

Status Likely cause Response
400 Invalid parameters or target cannot be captured. Check URL protocol and encoding, option names and values, and whether the target is reachable.
401 Invalid or revoked key. Verify the server-side key and rotate it if needed; do not expose it publicly.
402 Monthly screenshot quota exhausted. Check account usage and plan limits before retrying.
403 Requested feature is not supported by the plan. Remove the option or confirm plan support in current documentation.
429 Rate limit exceeded. Reduce request-start rate, honor any retry guidance, and retry with backoff.
500 Internal capture failure. Record the URL and response details; retry selectively with a delay.

The API documentation describes a leaky-bucket limit of 20 requests per second with a burst size of 400; requests exceeding the burst are terminated with HTTP 429. It also says exact repeated requests that fail are rate-limited to five per hour. Avoid tight retry loops: retry transient failures with exponential backoff and jitter, cap attempts, and do not repeatedly submit the same failing URL. Treat 400, 401, 402, and 403 as issues to fix before retrying rather than transient errors.

8. Performance, reliability, and cost considerations

  • Completion time: Sequential jobs are easy to reason about; bounded concurrency can reduce total elapsed time because independent requests overlap. Actual capture duration varies by target page and options. No fixed throughput should be assumed.
  • Rate compliance: Concurrency controls in-flight requests. For sustained batches, separately pace request starts to remain within the documented rate.
  • Partial completion: Persist results per URL and retry only eligible failures. This prevents one failed target from discarding successful captures.
  • Output association: Use a stable index or an encoded hash in output filenames and maintain a manifest with original URL and status. Avoid using raw URLs as filenames.
  • Quota and plan: Every URL means a separate API request and screenshot attempt. Track usage and check the account’s current quota and option restrictions. The dossier does not establish ApiFlash pricing, so consult its current account or pricing information rather than assuming a cost per image.
  • Cache behavior: Decide whether repeated captures should use cached content or request fresh content. Use documented freshness and TTL controls in a way that matches the job’s freshness requirements.

Or skip the browser setup

For a one-call-per-URL workflow with clean captures, ScreenshotNeo is a website screenshot API and MCP server for developers. Its API returns a PNG, JPEG, WebP, or PDF; see the ScreenshotNeo API docs.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Repeat the call for each URL in your list, using an HTTP client’s query-parameter encoder. ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot, and each of those steps can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers say which page verdict and billing outcome applied. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up free for 1,000 screenshots a month, no card required.

FAQ

Does ApiFlash accept a list of URLs in one capture request?

The documented multi-URL pattern is one authenticated capture request per target URL. Your own script can coordinate those independent requests.

Yes. The documentation describes response_type=json for a JSON response containing a screenshot link, with optional HTML or text extraction.

Can I safely call ApiFlash directly from a public webpage?

Do not place the access key in browser-visible code. Use a server-side integration that validates targets and limits caller request rates.

Why did only some URLs fail?

Each capture is an independent request. A particular target may be invalid, unavailable, blocked, or incompatible with selected options even when other pages in the batch succeed; report and retry results individually.