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-Keyper 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
ThreadPoolExecutoras 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
429response means slow down and retry after the number of seconds in theRetry-Afterheader. 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_bannersandblock_ads, so the screenshots show the page and not a consent dialog.- A
devicepreset (for example a phone) when you need the mobile layout of every page. full_pagewithlazy_loadfor 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.
Related jobs
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.