· Updated · Guides · By Simon Chiu

How to Choose a Screenshot API: A Scored Checklist

How to choose a screenshot API: a weighted checklist, a scoring sheet, and a Node script that tests any vendor on your own URLs before you commit.

Line illustration of the RenderScreenshot mascot scoring options on a checklist

To choose a screenshot API, write down what your captures must do, give each requirement a weight, then send the same set of your own URLs through every vendor on your shortlist and score what comes back. Marketing pages all claim speed and quality. Your URLs, your formats and your volume are the only test that predicts what you will get in production.

This guide gives you the three pieces you need: a weighted checklist, a scoring sheet, and a small Node.js script that measures latency, success rate and output size for any vendor. It is the method behind our screenshot API comparison.

Disclosure: I build RenderScreenshot. The checklist and the script are vendor-neutral, and the last section lists where our own product scores low.

Step 1: Decide whether you need an API at all

A screenshot API is a good fit when you need captures inside a product or a pipeline (social cards, link previews, archives, monitoring) and you do not want to run Chrome yourself. If you need multi-step automation (log in, click through a flow, fill forms), a hosted browser that you drive with Playwright or Puppeteer may fit better. The trade-offs are covered in screenshot API vs headless browser API.

If you only need a few captures by hand, a browser extension or the Chrome command line is enough. Skip the rest of this page.

Step 2: Weight the criteria before you look at vendors

Set the weights first. If you score vendors before you agree on weights, the team tends to tune the weights to fit the vendor it already likes.

Criterion Default weight (1-5) How to test it A 5 looks like A 1 looks like
Success rate on your URLs 5 Script below, 3+ runs per URL Every run returns a valid image Timeouts or blank images on common pages
Visual correctness 5 Open every output and use the rubric in Step 4 Fonts, lazy images and layout match a real browser Missing images, cookie walls, wrong fonts
Latency (p50 and p95) 3 Script below p95 fits inside your request budget p95 forces you into async jobs you did not plan for
Cost at your volume 4 Step 5 formula Fits your budget with headroom Next tier needed within months
Parameters you need 4 Map each requirement to a documented parameter Every requirement is a documented option You need custom JavaScript for basics
Embedding and auth 3 Read the auth docs Signed, expiring URLs for public <img> tags Only a secret key in the query string
Caching and delivery 2 Check response headers Cache control plus a CDN URL you can store Every request re-renders and is billed
SDK for your language 2 Install it and run one call Maintained SDK with typed errors Raw HTTP only
Error contract 2 Request a bad URL and a timeout Typed errors with a retryable flag HTTP 500 with an HTML body
Data handling depends Read the DPA and the retention policy Retention you can configure, DPA available No statement on where captures are stored

Change the weights for your use case. For social cards, visual correctness and caching matter most. For website archiving, success rate on long pages and storage options matter most. For an AI agent tool, latency and the error contract matter most.

The scoring sheet

Score each vendor from 1 to 5 on every row, then compute:

weighted score = sum(weight x score) / sum(weight x 5) x 100

Keep the raw scores, not only the total. A vendor that scores 88 overall but 2 on success rate is not a vendor you can ship. Treat any score of 1 or 2 on a weight-5 row as a veto.

Step 3: Build a URL list that looks like your traffic

The most common evaluation mistake is to test https://example.com and a few famous homepages. Those pages are simple, fast and probably cached already by every vendor. Build a list of 20 to 30 URLs from your real workload instead:

  • 5 pages that are typical for your product (your median case).
  • 5 pages with lazy-loaded images or infinite scroll.
  • 3 pages with cookie banners or consent walls in the regions you care about.
  • 3 single-page apps that load data after the first paint.
  • 3 pages with web fonts, emoji or non-Latin scripts.
  • 2 very long pages, if you need full-page captures.
  • 2 pages that you know are slow or fragile.

Put the list in a text file, one URL per line. Lines that start with # are comments.

Step 4: Run the same URLs through every vendor

The script below sends each URL to each vendor several times and writes one CSV row per attempt. It checks the first bytes of the response to confirm that it really got an image, because some APIs return HTTP 200 with an HTML error page.

// vendor-eval.mjs: run the same URLs through several screenshot APIs and record what came back.
// Usage: node vendor-eval.mjs urls.txt 3 > results.csv
import { readFile } from 'node:fs/promises';

const [, , urlFile = 'urls.txt', runsArg = '3'] = process.argv;
const RUNS = Number(runsArg);
const TIMEOUT_MS = 60_000;

// One entry per vendor. Each returns the fetch() arguments for one capture.
// Keys come from the environment; a vendor with no key is skipped.
const vendors = {
  renderscreenshot: {
    key: process.env.RS_API_KEY,
    request: (url, key) => [
      `${process.env.RS_BASE_URL ?? 'https://api.renderscreenshot.com'}/v1/screenshot`,
      {
        method: 'POST',
        headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ url, viewport: { width: 1280, height: 800 }, output: { format: 'png' } }),
      },
    ],
  },
  // Most screenshot APIs accept a GET with the target in a query string.
  // Put the vendor's documented URL in VENDOR_B_TEMPLATE, with {url} and {key} placeholders.
  vendor_b: {
    key: process.env.VENDOR_B_KEY,
    request: (url, key) => [
      process.env.VENDOR_B_TEMPLATE
        .replace('{url}', encodeURIComponent(url))
        .replace('{key}', encodeURIComponent(key)),
      {},
    ],
  },
};

const MAGIC = {
  png: [0x89, 0x50, 0x4e, 0x47],
  jpeg: [0xff, 0xd8, 0xff],
  webp: [0x52, 0x49, 0x46, 0x46],
};
const imageKind = (buf) =>
  Object.keys(MAGIC).find((k) => MAGIC[k].every((b, i) => buf[i] === b)) ?? 'not-an-image';

async function capture(vendor, url) {
  const [endpoint, init] = vendor.request(url, vendor.key);
  const started = performance.now();
  try {
    const res = await fetch(endpoint, { ...init, signal: AbortSignal.timeout(TIMEOUT_MS) });
    const buf = Buffer.from(await res.arrayBuffer());
    return {
      status: res.status,
      ms: Math.round(performance.now() - started),
      bytes: buf.length,
      kind: res.ok ? imageKind(buf) : 'error-body',
    };
  } catch (err) {
    return { status: 0, ms: Math.round(performance.now() - started), bytes: 0, kind: err.name };
  }
}

const urls = (await readFile(urlFile, 'utf8')).split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
console.log('vendor,url,run,status,ms,bytes,kind');

for (const [name, vendor] of Object.entries(vendors)) {
  if (!vendor.key) {
    console.error(`skip ${name}: no key in the environment`);
    continue;
  }
  for (const url of urls) {
    for (let run = 1; run <= RUNS; run++) {
      const r = await capture(vendor, url);
      console.log([name, JSON.stringify(url), run, r.status, r.ms, r.bytes, r.kind].join(','));
    }
  }
}

Add one entry per vendor. For a vendor that takes a GET request, copy the vendor_b block and set its template from the vendor's docs, for example VENDOR_B_TEMPLATE='https://api.vendor.test/take?url={url}&access_key={key}'. Run it from a server in the region where your production code runs, not from your laptop on office Wi-Fi.

Then summarize the CSV:

// summarize.mjs: p50/p95 latency, success rate and median bytes per vendor from results.csv
import { readFile } from 'node:fs/promises';

const rows = (await readFile(process.argv[2] ?? 'results.csv', 'utf8'))
  .trim().split('\n').slice(1)
  .map((line) => {
    const cells = line.match(/("[^"]*"|[^,]+)/g);
    const [vendor, , , status, ms, bytes, kind] = cells;
    return { vendor, ok: status === '200' && kind !== 'not-an-image' && kind !== 'error-body', ms: +ms, bytes: +bytes };
  });

const pct = (sorted, p) => sorted[Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1)];

for (const vendor of [...new Set(rows.map((r) => r.vendor))]) {
  const all = rows.filter((r) => r.vendor === vendor);
  const ok = all.filter((r) => r.ok);
  const ms = ok.map((r) => r.ms).sort((a, b) => a - b);
  const bytes = ok.map((r) => r.bytes).sort((a, b) => a - b);
  console.log(
    `${vendor}: success ${ok.length}/${all.length}, p50 ${pct(ms, 50) ?? '-'} ms, p95 ${pct(ms, 95) ?? '-'} ms, median ${pct(bytes, 50) ?? '-'} bytes`,
  );
}
node vendor-eval.mjs urls.txt 3 > results.csv
node summarize.mjs results.csv

Tested on 2026-10-15 with Node.js 22.18.0 against a local mock API (a vendor that returns PNGs and a vendor that returns 401), to check the CSV, the image check and the percentile output. The script has no dependencies.

Keep cold and warm runs apart

Most screenshot APIs cache results. With RenderScreenshot, caching is on by default, so the first run of a URL is a fresh render and the next runs may come from cache. The X-Cache-Hit response header tells you which one you got (see response headers). Report cold and warm numbers in separate columns. To measure only fresh renders, add cache: { enabled: false } to the request body, or add a unique query string to each target URL for vendors that do not let you turn the cache off.

Look at every image

Numbers do not catch a capture of a cookie wall. Open each output and score it with a short rubric:

Score Meaning
3 Matches what a person sees in a desktop browser
2 Small defect: one lazy image missing, a font fallback, a sticky header repeated
1 Usable only after a fix: consent banner on top, half-loaded SPA
0 Wrong: blank, error page, bot challenge, wrong page

Average the rubric per vendor and map it to the 1 to 5 "visual correctness" row.

Step 5: Compute cost per successful screenshot

List prices are per request. What you pay is per usable screenshot:

cost per usable screenshot = monthly plan price / (requests you can make x success rate)

Count cache hits too. If a vendor serves repeat requests from cache without charging, and your traffic repeats (social cards for the same posts, for example), the effective price drops. With RenderScreenshot, the X-Credits-Used header on every response shows what the request cost, so you can total it in the same CSV.

As a worked example with our own plans (checked 2026-10-15 on the pricing page): at 10,000 screenshots a month you need the Growth plan, $40 for 12,000 credits. At a 98% success rate on your URLs and no cache hits, that is $40 / (12,000 x 0.98), about $0.0034 per usable screenshot if you use the full allowance, or $0.0041 if you use only the 10,000 you need. Run the same arithmetic with each vendor's published price at your volume. Our pricing comparison has the per-volume tables.

Step 6: Watch for red flags

Red flag Why it matters
HTTP 200 with an HTML or JSON error body Your code saves a broken file and reports success
No timeout parameter, or a fixed short timeout Slow pages fail with no way to tune
API key only in the query string The key leaks into logs and HTML if you embed images
No way to wait for a selector Single-page apps capture as loading spinners
Pricing that charges for cache hits Repeat traffic costs the same as new renders
No status page or changelog You will not know when behaviour changes

A decision table by use case

Your use case Raise the weight of Lower the weight of
Social cards and link previews Visual correctness, caching, signed URLs Full-page support
Archiving and compliance Success rate on long pages, storage upload, metadata p50 latency
Monitoring and change detection Success rate, cost at volume, scheduling SDK polish
AI agents and tools p95 latency, error contract Embedding
PDFs and documents PDF options, fonts Image formats

Where RenderScreenshot scores low

Score us with the same sheet. Places where we will lose points today:

  • Python SDK distribution: the Python SDK installs from GitHub until the PyPI release is out. Node, Ruby, PHP, Java and Go install from their usual registries.
  • Very high volume on self-serve plans: the largest self-serve plan is Scale (60,000 credits a month). Above that you talk to us about a custom plan.
  • Browser automation: we render captures, we do not give you a remote browser to drive. If you need multi-step flows, a hosted browser is the better tool.

Where we expect to score well: the signed URL model for public embedding, presets such as og_card (1200x630), a public CDN link in the X-Cache-URL header, typed errors with a retryable flag, and official SDKs in six languages.

Run the checklist with a free key

You can put RenderScreenshot through this exact test on the Free plan (150 credits a month, no credit card), which covers a 25-URL list at 3 runs with room to spare. Create a free key, set RS_API_KEY, and run vendor-eval.mjs next to the vendors you are already considering.

Related in this topic