Take screenshots of many URLs
Capture many pages with the Screenshot API: run requests in parallel inside the account limit, pace to the route limit, retry a 429 and re-mint expired links.
Updated
The Screenshot API takes one URL per request. To capture a list of pages, run several requests at the same time from your own code. This guide shows the limits that apply, a worker pool that stays inside them, and how to get new download links later.
The limits
| Limit | Value | When you pass it |
|---|---|---|
| Web Data requests in progress, per account | 6 | The next request fails at once with 429 and error: "rate_limited". |
POST /api/v1/screenshot per minute | 30 | 429 with a Retry-After header in seconds. |
POST /api/v1/artifacts/link per minute | 120 | 429 with a Retry-After header. |
The 6 slots are shared by all Web Data calls of the account: scrapes, crawled pages, screenshots and site reports. When the shared browser pool is busy, a request can also wait up to 15 seconds for a browser and then fail with 429.
Each capture costs 5 credits, so 200 URLs cost at most 1,000 credits. A capture that fails is not charged. When the balance runs out, the answer is 402 with error: "billing_blocked". Stop the run then, because every later request fails the same way.
The current numbers are on the limits page.
One capture
Test one request first:
curl -X POST https://api.wayfern.com/api/v1/screenshot \
-H "Authorization: Bearer $WAYFERN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/products/1",
"capture": { "format": "jpeg", "quality": 80, "blockAds": true }
}' A worker pool in Node.js
This script runs 5 captures at once, which leaves one slot for other calls. It starts at most 30 requests a minute, retries a 429 after a wait, and writes each result to a file.
import { writeFile } from "node:fs/promises";
const API = "https://api.wayfern.com/api/v1/screenshot";
const TOKEN = process.env.WAYFERN_API_TOKEN;
const WORKERS = 5;
const STARTS_PER_MINUTE = 30;
const urls = ["https://example.com/products/1", "https://example.com/products/2"];
const results = [];
let next = 0;
let lastStart = 0;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function paced() {
const gap = 60_000 / STARTS_PER_MINUTE;
const wait = lastStart + gap - Date.now();
lastStart = Math.max(Date.now(), lastStart + gap);
if (wait > 0) await sleep(wait);
}
async function capture(url) {
for (let attempt = 1; attempt <= 5; attempt++) {
await paced();
const res = await fetch(API, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
url,
capture: { format: "jpeg", quality: 80, blockAds: true, failIfContains: ["Access denied"] },
}),
});
const body = await res.json();
if (res.ok) return { url, key: body.data.screenshot.key, link: body.data.screenshot.url };
if (res.status === 402) throw new Error(`Out of credits: ${body.message}`);
if (res.status !== 429) return { url, error: body.error, message: body.message };
const retryAfter = Number(res.headers.get("retry-after"));
await sleep(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 2000 * attempt);
}
return { url, error: "rate_limited", message: "Gave up after 5 attempts" };
}
async function worker() {
while (next < urls.length) {
const url = urls[next++];
results.push(await capture(url));
}
}
await Promise.all(Array.from({ length: WORKERS }, worker));
await writeFile("screenshots.json", JSON.stringify(results, null, 2)); Notes on the script:
- A
429from the account limit has noRetry-After. The script then waits a little longer on each attempt. - Other errors, such as
content_matched,not_foundortimeout, are results about one URL. The script records them and moves on. - Store the
keyof each image. You need it to download the image after the link expires.
Get new links later
The url that comes with a capture is a signed link with a short life. The image is kept for 7 days. Exchange its key for a new link:
curl -X POST https://api.wayfern.com/api/v1/artifacts/link \
-H "Authorization: Bearer $WAYFERN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "key": "screenshots/USER_ID/9c1f4d02-7ab8-4e51-9d3f-2c60ba17e8c4.jpg" }' {
"success": true,
"data": {
"key": "screenshots/USER_ID/9c1f4d02-7ab8-4e51-9d3f-2c60ba17e8c4.jpg",
"url": "https://...",
"urlExpiresAt": "2026-10-29T10:15:00.000Z"
},
"credits": 0
} The call is free. It works for screenshots, slices and PDFs. A key that expired, or that belongs to another account, gives 404 with error: "not_found". The answer is the same in both cases.
Speed up the run
- Use
"format": "jpeg"for smaller files. - Capture the viewport only (
"fullPage": false) when you do not need the whole page. - Use
failIfContainsso that error pages fail early and cost nothing.
More on limits and 429 handling is in the rate limits guide. Every field is in the Screenshot API reference.