/api/v1/batchCapture up to 10 URLs in a single request and get every image back in one response. Each URL takes the same options as a single capture, so you can mix full-page shots, different viewports and different formats in the same batch.
Requires Pro or above. Free and Starter keys receive 403 PLAN_LIMIT. Capturing more than 10 URLs is a normal thing to want — see Capturing hundreds of URLs below.
| Field | Type | Required | Description |
|---|---|---|---|
| urls | array | Yes | 1 to 10 capture objects. More than 10 returns 400 INVALID_PARAMS. |
| urls[].url | string | Yes | Public http(s) URL to capture. |
| urls[].fullPage | boolean | No | Capture the entire scrollable page. Default false. |
| urls[].width / height | number | No | Viewport in px. Defaults 1280 × 720, up to 3840 × 2160. |
| urls[].format | string | No | png, jpeg or webp. |
| urls[].selector | string | No | Capture a single element instead of the page. |
| urls[].delay | number | No | Extra wait in ms before capturing (0–10000). |
| webhook_url | string | No | HTTPS endpoint notified when the batch finishes. |
Cookie banners and chat widgets are removed automatically on every item, exactly as on /v1/screenshot.
curl -X POST "https://captureapi.dev/api/v1/batch" \
-H "X-API-Key: cap_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"urls": [
{ "url": "https://example.com", "fullPage": true },
{ "url": "https://example.org", "width": 1920, "height": 1080 },
{ "url": "https://example.net", "format": "jpeg" }
]
}'{
"success": true,
"batch_id": "batch_m3k2j1_a8f2",
"total": 3,
"succeeded": 2,
"failed": 1,
"truncated": false,
"not_processed": [],
"duration_ms": 12480,
"results": [
{
"request_id": "req_m3k2j1_b4c1",
"url": "https://example.com",
"status": "completed",
"format": "png",
"width": 1280,
"height": 720,
"file_size": 184320,
"duration_ms": 4210,
"image": "data:image/png;base64,iVBORw0KGgo...",
"truncated": false,
"page_height": 4180,
"degraded": false,
"images_pending": 0
},
{
"request_id": "req_m3k2j1_b4c2",
"url": "https://blocks-robots.example",
"status": "failed",
"error_code": "TARGET_RENDERED_NOTHING",
"error_message": "The target page loaded but rendered no visible content...",
"duration_ms": 3120
}
],
"meta": { "plan": "pro", "remaining": 14997 }
}A batch runs inside a single 60-second function. Captures happen one after another, and how long each one takes depends entirely on the page. That means a batch of 10 heavy, full-page sites can genuinely run out of time.
When that happens the endpoint does not fail and does not quietly drop work. It stops before the ceiling, returns every capture it did complete, and tells you the rest:
truncated: true — the batch was cut short.not_processed — the URLs it never reached. These are not charged. Resubmit them in a new request.results with status: "failed" and an error code.let pending = urls;
while (pending.length > 0) {
const res = await fetch("https://captureapi.dev/api/v1/batch", {
method: "POST",
headers: {
"X-API-Key": process.env.CAPTURE_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ urls: pending.slice(0, 10).map((url) => ({ url })) }),
});
const body = await res.json();
save(body.results);
// Anything the time budget could not reach comes back here — and was not
// charged. Put it at the front of the next request.
pending = [...body.not_processed, ...pending.slice(10)];
}For anything beyond a handful of pages, loop the single-capture endpoint with a small amount of concurrency instead of using batch. It is faster in wall-clock terms, it fails one URL at a time instead of one request at a time, and you can write each image to disk as it arrives rather than holding a large JSON response in memory.
import fs from "node:fs/promises";
const API_KEY = process.env.CAPTURE_API_KEY;
const CONCURRENCY = 3; // stay under your plan's per-second burst limit
const urls = [
"https://example.com",
"https://example.org",
// ...as many as you like
];
async function capture(url) {
const params = new URLSearchParams({
url,
width: "1440",
full_page: "true",
block_cookie_banners: "true",
format: "png",
});
const res = await fetch(`https://captureapi.dev/api/v1/screenshot?${params}`, {
headers: { "X-API-Key": API_KEY },
});
if (!res.ok) {
const err = await res.json().catch(() => null);
throw new Error(err?.error?.message ?? `HTTP ${res.status}`);
}
const name = new URL(url).hostname.replace(/\W+/g, "-") + ".png";
await fs.writeFile(name, Buffer.from(await res.arrayBuffer()));
return name;
}
// Simple concurrency-limited queue.
const queue = [...urls];
await Promise.all(
Array.from({ length: CONCURRENCY }, async () => {
let url;
while ((url = queue.shift())) {
try {
console.log("saved", await capture(url));
} catch (e) {
console.error("failed", url, e.message);
}
}
})
);Keep concurrency at or below your plan’s per-second limit (Pro: 10/s, Business: 20/s) to avoid 429 RATE_LIMIT_EXCEEDED.
| Status | Code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or unknown API key. |
| 403 | PLAN_LIMIT | Batch requires Pro or above. |
| 400 | INVALID_PARAMS | Empty list, more than 10 URLs, a missing url, or a blocked host. |
| 413 | PAYLOAD_TOO_LARGE | Request body over 5MB. |
| 429 | RATE_LIMIT_EXCEEDED | Monthly quota is smaller than the batch, or too many requests per second. |