How to Screenshot Multiple URLs Using the ScreenshotOne API
Send up to 20 screenshot requests in one ScreenshotOne bulk call. Learn when screenshots render, how to batch larger jobs, and how to handle failures.
To screenshot multiple URLs with the ScreenshotOne API, send a JSON POST request to https://api.screenshotone.com/bulk. Put each page in the requests array, and put shared screenshot settings in options. A bulk request supports up to 20 items. By default, the response contains screenshot URLs and rendering happens when those URLs are downloaded; set execute: true to render before the response returns. ScreenshotOne’s bulk screenshots documentation describes the endpoint and its limits.
1. Send a bulk screenshot request
Here is a cURL example that renders three pages with a shared viewport and ad blocking. One request overrides the shared settings. Replace the placeholder with your own key; do not publish or commit a real key.
curl --fail-with-body --silent --show-error \
--request POST "https://api.screenshotone.com/bulk" \
--header "Content-Type: application/json" \
--data '{
"access_key": "YOUR_SCREENSHOTONE_ACCESS_KEY",
"execute": true,
"options": {
"viewport_width": 1280,
"viewport_height": 1024,
"block_ads": true
},
"requests": [
{"url": "https://example.com"},
{"url": "https://example.org", "viewport_width": 1440},
{"url": "https://example.net", "block_ads": false}
]
}'
The options object supplies defaults to each item. An item may override an option by setting it inside that request object. The endpoint accepts the regular screenshot options. Each request needs a screenshot source such as a URL, HTML, or Markdown; the official example shows all three. For the full option list, see ScreenshotOne’s screenshot options reference.
Authentication can be included in the JSON body as above, sent as the X-Access-Key header, or included in the query string. Prefer a server-side secret or environment variable. Avoid putting keys in browser code, public URLs, logs, or source control. See the API key guidance.
2. Choose when screenshots should render
Lazy rendering: default behavior
Without execute: true, bulk is a wrapper that returns an array of individual screenshot URLs. The image is generated when a client downloads its returned URL, so a successful bulk response alone does not mean every screenshot is already rendered. This can suit a workflow that wants URLs first and can fetch them later.
Render before returning: execute: true
Set execute to true when the caller needs an execution result for every item immediately. The endpoint waits for the screenshot jobs; allow enough time for the whole batch. The response includes per-request execution status, and failed items include error details. Handle each item separately instead of assuming a single successful item means the whole batch succeeded.
The request above uses execute: true. Set it to false or omit it for the default lazy behavior.
3. Run the same request from Python
This example uses the requests package. It submits the batch, checks the HTTP response, and prints the JSON result so you can inspect each returned URL or execution status.
import json
import os
import requests
access_key = os.environ["SCREENSHOTONE_ACCESS_KEY"]
payload = {
"access_key": access_key,
"execute": True,
"options": {
"viewport_width": 1280,
"viewport_height": 1024,
"block_ads": True,
},
"requests": [
{"url": "https://example.com"},
{"url": "https://example.org", "viewport_width": 1440},
{"url": "https://example.net", "block_ads": False},
],
}
response = requests.post(
"https://api.screenshotone.com/bulk",
json=payload,
timeout=180,
)
response.raise_for_status()
result = response.json()
print(json.dumps(result, indent=2))
Set the environment variable before running the script, for example with export SCREENSHOTONE_ACCESS_KEY='your-key' in a Unix-like shell. The timeout is a client-side waiting limit, not a claim about how long rendering takes. Choose a value appropriate for your batch and network.
4. Run it from Node.js
This example uses the built-in fetch available in current Node.js versions. It checks for an HTTP error and prints the JSON body.
const accessKey = process.env.SCREENSHOTONE_ACCESS_KEY;
if (!accessKey) throw new Error("Set SCREENSHOTONE_ACCESS_KEY first");
const payload = {
access_key: accessKey,
execute: true,
options: {
viewport_width: 1280,
viewport_height: 1024,
block_ads: true,
},
requests: [
{ url: "https://example.com" },
{ url: "https://example.org", viewport_width: 1440 },
{ url: "https://example.net", block_ads: false },
],
};
const response = await fetch("https://api.screenshotone.com/bulk", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(180_000),
});
const body = await response.text();
if (!response.ok) {
throw new Error(`ScreenshotOne returned HTTP ${response.status}: ${body}`);
}
console.log(JSON.stringify(JSON.parse(body), null, 2));
Set SCREENSHOTONE_ACCESS_KEY in the process environment before starting Node.js. If you use lazy mode, download each returned URL separately; the bulk response contains URLs, not image bytes.
5. Process more than 20 URLs
The documented ceiling is 20 requests in one bulk call. Split larger input into chunks of at most 20. For small jobs, a sequential loop is simple and avoids starting too much work at once. For larger jobs, put chunks on a queue and control how quickly workers submit them.
function chunks(items, size) {
const result = [];
for (let i = 0; i < items.length; i += size) {
result.push(items.slice(i, i + size));
}
return result;
}
for (const batch of chunks(urls, 20)) {
const response = await fetch("https://api.screenshotone.com/bulk", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
access_key: process.env.SCREENSHOTONE_ACCESS_KEY,
execute: true,
options: { viewport_width: 1280, viewport_height: 1024 },
requests: batch.map((url) => ({ url })),
}),
});
if (!response.ok) {
throw new Error(`Bulk request failed: HTTP ${response.status} ${await response.text()}`);
}
const result = await response.json();
// Persist or inspect this batch before moving on.
}
This snippet assumes urls is an array of URL strings and that you have defined it. For production workloads, persist the input and each batch result so a process restart does not lose track of completed work. Retry only failed work where possible; blindly resubmitting successful items can spend requests again.
6. Option: reuse a page with optimize
optimize: true is intended for executing multiple captures of the same source with different settings, such as several viewports. It only works when execute: true is also set. For example, the shared options can define a common URL and each request can change the viewport:
{
"access_key": "YOUR_SCREENSHOTONE_ACCESS_KEY",
"execute": true,
"optimize": true,
"options": {
"url": "https://example.com",
"viewport_width": 1280,
"viewport_height": 1024
},
"requests": [
{"viewport_width": 360, "viewport_height": 640},
{"viewport_width": 736, "viewport_height": 414}
]
}
Do not assume this always reduces runtime. A viewport change may cause a reload, and options such as changing ad blocking may also require one. Compare it with your actual pages and settings before relying on it for throughput planning.
7. Control throughput and retries
Bulk requests count against the same one-minute request-start bucket as regular screenshot requests. Read the usage endpoint and use concurrency.remaining and concurrency.reset to decide when to submit more work. These fields describe how many requests can be started in the current bucket and when it resets; they are not a count of render processes currently running.
For an in-process task, use a queue with bounded retries and backoff. If multiple workers share the key, coordinate them through one rate-aware queue so each does not independently consume the apparent remaining capacity. For retries that must survive restarts or run across several workers, use a durable queue such as Redis/BullMQ or SQS. The official guide recommends draining queued work according to the usage values; it does not publish a universal worker count or throughput benchmark.
When a job does not need to block the initiating request, consider the documented asynchronous rendering and webhook pattern. ScreenshotOne describes uploading results to S3-compatible storage and sending execution results to a webhook. That pattern shifts completion handling to a callback, so validate webhook results and make your receiving handler safe to retry.
8. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| The bulk response has URLs, but no images | Lazy mode returns screenshot URLs before the screenshots are rendered. | Download each returned URL to trigger rendering, or set execute: true when you need execution results in the initial response. |
| Some items fail while others succeed | Execution status is reported per request; one item’s failure does not imply every item failed. | Inspect each status and error body. Record successful items and retry only failures after addressing their cause. |
| The batch exceeds the request limit | There are more than 20 items in one bulk request. | Split the input into batches of no more than 20. |
concurrency_limit_reached or concurrency error |
The current one-minute request-start bucket is exhausted. | Check the usage endpoint and wait until concurrency.reset; queue and drain only the requests allowed by concurrency.remaining. See the concurrency error guide. |
| Authentication error | The key is missing, invalid, copied from the wrong organization, or exposed and replaced. | Send the current access key in the body, header, or query string. Keep it private and check the key documentation. |
| The caller times out waiting | Many screenshots are being executed synchronously, or the client timeout is too short for the workload. | Increase the client wait limit appropriately, reduce batch size, use lazy URLs, or use an asynchronous webhook workflow. |
optimize appears to have no benefit |
Optimization is workload-dependent; viewport changes and some option changes can trigger reloads. | Keep execute: true and compare optimized and unoptimized requests for the same representative pages. Do not assume a speedup. |
| A screenshot URL is exposed in a public page | The URL or request includes credentials or otherwise reveals access details. | Keep API calls server-side and avoid publishing unsigned URLs containing a key. Rotate a key if it has been exposed. |
9. Performance, reliability, and cost
- Performance: A single bulk call reduces the number of client submissions, but it does not remove the per-request bucket limit. Lazy rendering delays the work until download; execution mode waits for the requested renders. Benchmark with your own sites and options rather than assuming the API call’s response time represents the total image delivery time.
- Reliability: Capture per-item outcomes, make retries bounded, and checkpoint completed work. Use durable queue storage when retries must survive a worker restart. Do not automatically retry permanent input or authentication errors.
- Cost and entitlement: The supplied documentation establishes the 20-item bulk ceiling and usage fields, but not current plan prices or plan-specific request entitlements. Check your account and current pricing before estimating a production workload. A bulk wrapper should not be treated as evidence that a batch is one billable screenshot.
10. Or skip the browser setup
If you want a single screenshot request without operating a browser or submitting a ScreenshotOne bulk job, ScreenshotNeo is a website screenshot API and MCP server. Its one-call API returns a screenshot in PNG, JPEG, or WebP, or a PDF. See the ScreenshotNeo 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}`);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. An 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 screenshots. Sign up for ScreenshotNeo’s free plan.
11. Frequently asked questions
Can one item be HTML or Markdown instead of a URL?
Yes. The bulk request can contain URL, HTML, or Markdown sources. Use JSON request bodies for this endpoint and keep large content within the service’s applicable request constraints.
Does bulk return image files directly?
The documented response returns screenshot URLs. In execution mode it also includes per-request execution status; download the returned URLs when you need image bytes.
Can the same API key be used by several workers?
Yes, but workers share the key’s request-start bucket. Coordinate their submissions using the usage endpoint so they do not exceed the remaining allowance.
Should I use bulk for just a few pages?
It is useful when one submission and shared defaults simplify the caller. For jobs that need durable scheduling, retries, or independent completion handling, a queue may fit better.


