Menu
Sign up

Bulk website screenshots: capture a list of URLs from a CSV, measured

Screenshot hundreds of URLs from a CSV with a short script: the concurrency we measured, webhooks for long lists, safe retries, and the quota maths.

Published November 8, 2026 · 5 min read

A list of URLs that all need a screenshot is a common job: every page of a site before a redesign, every competitor's pricing page, every link in a directory that needs a thumbnail, every landing page in an ad campaign. One screenshot is one HTTP request; a thousand are a loop — but a loop that has to handle parallel requests, failures, retries and cost. This page shows a small script for it and what we measured when we ran it.

How we measured. On 2026-11-08 we sent viewport screenshots of 10 public home pages (example.com, iana.org, w3.org, python.org and others) through our API with ?wait=true, first one at a time, then five at a time, from a Python script on our development stack. Times include each page's own loading, so they vary with the sites; treat them as an indication, not a benchmark.

The script

A CSV with one URL per row in, a CSV with each screenshot's image URL out:

import csv, json, uuid, concurrent.futures as cf
import requests

API = "https://shareglint.com/v1/screenshots?wait=true"
HEADERS = {"Authorization": "Bearer sk_live_..."}

def capture(url: str) -> dict:
    r = requests.post(
        API,
        json={"url": url, "block_cookie_banners": True, "format": "jpeg"},
        headers={**HEADERS, "Idempotency-Key": str(uuid.uuid5(uuid.NAMESPACE_URL, url))},
        timeout=90,
    )
    data = r.json()
    return {"url": url, "status": r.status_code, "image": data.get("url"), "error": data.get("error")}

with open("urls.csv") as f:
    urls = [row[0] for row in csv.reader(f) if row]

with cf.ThreadPoolExecutor(max_workers=5) as pool:
    results = list(pool.map(capture, urls))

with open("screenshots.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["url", "status", "image", "error"])
    writer.writeheader()
    writer.writerows(results)

Three details that matter at scale:

  • A fixed Idempotency-Key per URL (here derived from the URL). If the script dies at URL 600 and you run it again, the first 599 requests return their earlier responses instead of new captures — no double cost, no duplicates. Change the key when you want fresh captures, for example by adding the date. We ran the script above twice on the same three URLs: the second run returned the same three image URLs and used no quota.
  • A results file, not just images. Keep the image URL, the status and the error per row, so you can retry only the failures.
  • Download the images you want to keep. Our copies are kept for 365 days on paid plans.

What we measured: one at a time vs five at a time

Total for 10 URLs Time per screenshot
One at a time 57.0 s 4.4–6.6 s
Five in parallel 30.5 s 4.6–16.1 s

Five in parallel finished the list 1.9 times faster — not five times. Each capture is a real browser loading a real page, and requests beyond what is free at that moment wait their turn on our side, so individual captures took longer while the total still fell. In practice that means:

  • Use a small pool — four to eight workers, with Python's ThreadPoolExecutor as above or any job queue. More rarely finishes sooner and makes timeouts more likely.
  • Set a generous timeout (we used 90 s) and retry only on errors, not on slowness.
  • Your plan's per-minute API limit caps how fast you can send requests; a 429 response means slow down and retry after the number of seconds in the Retry-After header. A simple back-off — wait, then retry the same request with the same idempotency key — is all a script needs.

Long lists: send, then let the results come to you

For hundreds or thousands of URLs, do not hold a connection open per screenshot. Send each request without ?wait=true and with a webhook_url; the API answers at once with an id, and when the capture is done we POST screenshot.completed (or screenshot.failed) with the image URL to your endpoint:

{ "url": "https://example.com/", "full_page": true, "lazy_load": true,
  "webhook_url": "https://your-app.example/hooks/screenshots" }

Your endpoint matches the id to the row in your list and stores the image. Answer the webhook quickly with a 2xx and do the slow work — downloading the file, writing to storage — afterwards, so a burst of finished screenshots does not time out your endpoint. If you cannot receive webhooks, poll GET /v1/screenshots/{id} every few seconds instead. How to verify our webhook signature is shown in the JavaScript guide.

The maths

Job Renders
1,000 viewport screenshots 1,000
1,000 full-page screenshots 2 × 1,000
The same list again within 24 hours, same options 0
A failed capture (the page did not load) 0 — the render is given back

Compare that with the monthly quotas on the pricing page: Starter includes 2,000 renders, Pro 10,000. Decide viewport or full page per list before you start — full pages cost more and are larger files.

Options worth setting for a whole list

  • block_cookie_banners and block_ads, so the screenshots show the page and not a consent dialog.
  • A device preset (for example a phone) when you need the mobile layout of every page.
  • full_page with lazy_load for archives; viewport only for thumbnails.
  • format: "jpeg" for photos and long pages, PNG for interfaces and text.

Pages that need a login, sit on a private network or run on localhost cannot be captured through our API; the screenshot browser reaches only public addresses. For those, run Playwright or Puppeteer inside your network.

To repeat the same list every day, see scheduled screenshots; to show the results in a spreadsheet next to each URL, the Google Sheets IMAGE function puts each image in its cell. For a quick one-off capture without code, our free website screenshot tool takes one URL at a time.

FAQ

How do I take screenshots of multiple URLs at once?

Read the URLs from a CSV and send one screenshot request per URL, a few at a time. In our test 10 URLs took 57.0 s one after another and 30.5 s with five in parallel. For long lists, send the requests without waiting and give each a webhook_url, so results arrive as they finish.

How many screenshots can I take in parallel?

A handful. Requests beyond that queue on our side, so each takes longer while the total still drops: with five in parallel, our 10 URLs finished 1.9 times faster, but single captures took 4.6–16.1 s instead of 4.4–6.6 s. Your plan's per-minute API limit caps how fast you can send requests.

How much does a bulk screenshot job cost?

One render per viewport screenshot and 2 per full page. 1,000 viewport screenshots are 1,000 renders; the same URL with the same options within 24 hours is free, so re-running a list the same day only pays for new URLs.

What happens if my script crashes halfway through the list?

Run it again with the same Idempotency-Key for each URL. A request with a key and body we have already seen returns the first response instead of capturing again, so finished URLs cost nothing and only the missing ones are captured.