ScreenshotNeo

BlogHow-to

How to Take Screenshots of Multiple URLs with Browserless

Capture screenshots from many URLs with Browserless using parallel REST requests. Learn how to configure captures, handle failures, and control concurrency.

By the ScreenshotNeo team4 October 20267 min read

To take screenshots of multiple URLs with Browserless, send one authenticated POST request to its /screenshot REST endpoint for each URL. Run independent requests concurrently when your Browserless plan allows it, check each response for errors, and save successful image bytes to distinct filenames. Each REST request opens an independent browser session, so batch concurrency must fit your plan’s session limit.

1. Get a Browserless API token

Create or access your Browserless account and get an API token from the account dashboard. The REST endpoint authenticates with a token query parameter. Keep the token in an environment variable rather than committing it to source control. See the Browserless Screenshot API documentation and the concurrent sessions guide.

2. Choose capture settings

Each URL gets its own JSON request body. A basic full-page PNG request looks like this:

{"url":"https://example.com/","options":{"fullPage":true,"type":"png"}}

Choose settings according to the output you need:

Setting Use
fullPage Capture the document beyond the initially visible viewport.
type Select a supported image format such as PNG or JPEG.
quality Set image quality for formats that support it.
clip Capture a defined portion of the page.
viewport Set the browser viewport dimensions.
deviceScaleFactor Control pixel density for the captured viewport.
Selector capture Capture a specific page element when a full-page image is unnecessary.

Consult the API documentation for the precise shape and accepted values of each option. If a page loads images or other content lazily, Browserless’s FAQ notes that scrollPage: true may be needed before a full-page capture.

3. Capture multiple URLs with cURL

For a small batch, issue one request per URL. This shell example uses sequential requests and distinct output names. It stops on HTTP errors so an error response is not silently saved as an image.

export BROWSERLESS_TOKEN='YOUR_API_TOKEN_HERE'

curl --fail --silent --show-error \
  'https://production-sfo.browserless.io/screenshot?token='"$BROWSERLESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/","options":{"fullPage":true,"type":"png"}}' \
  -o screenshot-1.png

curl --fail --silent --show-error \
  'https://production-sfo.browserless.io/screenshot?token='"$BROWSERLESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://www.iana.org/","options":{"fullPage":true,"type":"png"}}' \
  -o screenshot-2.png

To parallelize shell jobs, run a bounded number of these requests at a time. Do not start an unbounded background process for a large URL list: each request uses a browser session, and plan concurrency applies.

4. Capture many URLs concurrently in Python

This runnable example uses a thread pool, checks HTTP status, and writes each response to a unique PNG path. Set MAX_WORKERS no higher than the concurrency available to your account and job runner.

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

import requests

TOKEN = os.environ["BROWSERLESS_TOKEN"]
ENDPOINT = "https://production-sfo.browserless.io/screenshot"
URLS = [
    "https://example.com/",
    "https://www.iana.org/",
]
MAX_WORKERS = 2  # Keep within your plan's concurrency limit.
OUT = Path("screenshots")
OUT.mkdir(exist_ok=True)


def capture(index, url):
    response = requests.post(
        ENDPOINT,
        params={"token": TOKEN},
        json={"url": url, "options": {"fullPage": True, "type": "png"}},
        timeout=90,
    )
    response.raise_for_status()
    path = OUT / f"screenshot-{index}.png"
    path.write_bytes(response.content)
    return url, path


with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
    futures = [pool.submit(capture, i, url) for i, url in enumerate(URLS, 1)]
    for future in as_completed(futures):
        try:
            url, path = future.result()
            print(f"Saved {url} to {path}")
        except requests.RequestException as error:
            print(f"Capture failed: {error}")

5. Capture many URLs concurrently in Node.js

This example uses Node.js’s built-in fetch and a small worker pool to cap simultaneous requests. Set MAX_WORKERS to a value your plan supports.

const token = process.env.BROWSERLESS_TOKEN;
if (!token) throw new Error('Set BROWSERLESS_TOKEN');

const endpoint = 'https://production-sfo.browserless.io/screenshot';
const urls = ['https://example.com/', 'https://www.iana.org/'];
const maxWorkers = 2;
let next = 0;

async function capture(index, url) {
  const response = await fetch(`${endpoint}?token=${encodeURIComponent(token)}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      url,
      options: { fullPage: true, type: 'png' },
    }),
  });
  if (!response.ok) {
    throw new Error(`${url}: HTTP ${response.status} ${await response.text()}`);
  }
  const bytes = Buffer.from(await response.arrayBuffer());
  const { writeFile } = await import('node:fs/promises');
  const path = `screenshot-${index}.png`;
  await writeFile(path, bytes);
  return { url, path };
}

async function worker() {
  while (true) {
    const index = next++;
    if (index >= urls.length) return;
    try {
      console.log('Saved', await capture(index + 1, urls[index]));
    } catch (error) {
      console.error('Capture failed:', error.message);
    }
  }
}

await Promise.all(
  Array.from({ length: Math.min(maxWorkers, urls.length) }, () => worker()),
);

6. Decide between sequential and parallel requests

Approach Good fit Trade-off
Sequential Small batches, strict resource limits, or simpler recovery. Total completion time accumulates across page loads.
Bounded parallel Independent URLs when the account has available concurrent sessions. Needs concurrency limits and per-URL failure handling.

Parallel execution can reduce batch wall time when sessions are available, but the documentation does not promise a universal concurrency maximum or a specific speedup. Confirm your account’s limit before submitting a large batch. Each REST call is an independent browser session.

7. Handle failures and make batch jobs reliable

  • Check the HTTP status before writing a response as an image. Error bodies may contain text or JSON rather than image bytes.
  • Keep a result per input URL: success path or failure details. One failed URL should not erase successful captures from the rest of the batch.
  • Retry only transient failures, with a small retry limit and backoff. Avoid retrying invalid URLs, authentication failures, or unsupported options unchanged.
  • Use deterministic filenames or a manifest that maps each output to its source URL. Sanitize URL-derived names and avoid collisions.
  • Choose timeouts that account for slow navigation and large full-page images. A client timeout ends your wait; it does not prove the page itself is invalid.
  • For repeatable jobs, record the requested settings alongside the result so format, viewport, and capture scope are clear.

8. When to use a browser connection instead

The REST screenshot endpoint is suitable for direct URL-to-image work. If a page requires interaction, explicit navigation control, or more tailored waiting, Browserless documents connecting with Puppeteer, Playwright, or BAP, navigating to the page, and then taking the screenshot. Browser launch configuration for REST calls can be supplied as query parameters or as a URL-encoded JSON launch payload. See the Browserless documentation for the browser connection and launch configuration details.

9. Troubleshooting

Symptom Likely cause Fix
Unauthorized or token error Missing, invalid, or incorrectly encoded token. Check the dashboard token and ensure it is passed as the token query parameter.
Rate or concurrency errors The batch exceeds available concurrent sessions. Lower the worker count and confirm the account plan’s concurrency allowance.
Output file is not an image An HTTP error response was saved as bytes. Check status before writing; log the response body for failures.
Page is cut off The request captured only the viewport. Enable fullPage for a full-document capture.
Lazy images are missing Content loads only after scrolling. Try the documented scrollPage: true option for full-page captures.
Capture times out or page is incomplete Slow navigation, delayed rendering, or page-specific behavior. Allow an appropriate client timeout; if interaction or explicit wait behavior is needed, use a browser connection and control navigation and waiting there.
Files overwrite each other Multiple requests use the same output filename. Assign a unique index or stable identifier to every URL.

10. Performance, reliability, and cost

For independent pages, bounded concurrency is the main way to avoid waiting for every capture in sequence. The practical ceiling is the lower of your Browserless account concurrency and the capacity of your own runner. Large full-page captures can produce larger responses and take longer to download and write than viewport captures. No published benchmark in the reviewed Browserless documentation supports a universal throughput or latency estimate.

For reliable jobs, combine bounded concurrency, status checks, per-URL outcomes, unique paths, timeouts, and restrained retries. The research sources establish the API workflow and concurrency behavior, but do not provide pricing figures; check your Browserless account plan for current costs and limits before running a large batch.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. A single GET request captures a URL as an image or PDF; the same request pattern can be repeated for a URL list. Its API accepts the parameter names used by other screenshot APIs, which can make switching straightforward. 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
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}`);

ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots.

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

FAQ

Does one Browserless REST request accept a list of URLs?

The documented multi-URL method is one independent /screenshot request per URL.

Can I capture the full page?

Yes. Set fullPage: true in the screenshot options. For lazy-loaded content, scrolling may be necessary.

How many requests can I run at once?

There is no single maximum for every account in the reviewed guide. Use the concurrency available on your plan.

Do I need Puppeteer for a basic capture?

No. The REST endpoint returns screenshot bytes directly. Use a browser connection when the page needs interaction or more explicit navigation and waiting controls.