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

LimitValueWhen you pass it
Web Data requests in progress, per account6The next request fails at once with 429 and error: "rate_limited".
POST /api/v1/screenshot per minute30429 with a Retry-After header in seconds.
POST /api/v1/artifacts/link per minute120429 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 429 from the account limit has no Retry-After. The script then waits a little longer on each attempt.
  • Other errors, such as content_matched, not_found or timeout, are results about one URL. The script records them and moves on.
  • Store the key of each image. You need it to download the image after the link expires.

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 failIfContains so 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.