Screenshot API vs Headless Browser API

Screenshot API vs headless browser API: the same five capture tasks in both, lines of code, billing models, cost per capture, and when each one wins.

By Simon Chiu · Updated

Disclosure: I build RenderScreenshot, which is a screenshot API, and it runs on a headless browser platform (Cloudflare's). So I pay for one category and sell the other. The code and line counts below are reproducible; the prices are quoted from each vendor's pricing page in October 2026, with links.

Short answer: a screenshot API is a function, capture(url, options) -> image. A headless browser API is a remote computer with Chrome on it that you program. For the five capture tasks below, the headless browser version took 55 lines of Puppeteer, and the screenshot API version took 12 lines. The headless browser can be cheaper per capture if it bills by the second, and it can do things a screenshot API cannot (log in, click through flows, scrape). If your job ends with "and save an image", the screenshot API is less code to write and less code to keep alive.

This page is the category-level view in our comparison hub. For vendor-level comparisons, see Browserbase vs Browserless and Browserless vs Cloudflare Browser Rendering.

The two categories

Screenshot API Headless browser API (browser as a service)
What you send A URL or HTML plus options (format, full_page, selector...) A WebSocket connection; then Puppeteer, Playwright or CDP commands
What you get back Image or PDF bytes, or JSON with a URL Whatever your code produces
Who writes the capture logic The vendor You
Typical billing Per successful request or credit Per unit of browser time, often plus concurrency
Caching of results Usually built in You build it
Multi-step flows (log in, click, fill forms) Limited to options the vendor exposes Anything a browser can do
Examples RenderScreenshot, ScreenshotOne, ApiFlash, Urlbox Browserless, Browserbase, Cloudflare Browser Rendering

Some products sit in both columns. Browserless and Cloudflare also expose REST endpoints that take a URL and return a screenshot, which behave like a basic screenshot API on top of the same browser time billing.

Five tasks, two implementations

We picked five tasks that cover most screenshot workloads:

  1. Viewport screenshot at 1280x800.
  2. Full-page screenshot, scrolled first so lazy-loaded images appear.
  3. Screenshot of one element.
  4. "Clean" capture: ad and tracker hosts blocked, cookie banner hidden.
  5. A4 PDF with background colors.

Headless browser API version (Puppeteer)

This connects to any hosted browser that accepts a Puppeteer WebSocket endpoint, which is how Browserless, Browserbase and similar services work. Leave BROWSER_WS unset and it launches local Chrome instead, which is how we tested it.

// Five capture tasks with Puppeteer against a remote (hosted) browser.
// BROWSER_WS=wss://... uses a headless browser API; unset = local Chrome for testing.
import puppeteer from 'puppeteer';
import { writeFile } from 'node:fs/promises';

const target = process.argv[2] ?? 'https://example.com';
const BLOCK = [/doubleclick\.net/, /googlesyndication\.com/, /google-analytics\.com/];

async function openBrowser() {
  if (process.env.BROWSER_WS) return puppeteer.connect({ browserWSEndpoint: process.env.BROWSER_WS });
  return puppeteer.launch();
}

async function withPage(fn) {
  const browser = await openBrowser();
  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1280, height: 800 });
    return await fn(page);
  } finally {
    await browser.close(); // on a hosted browser this ends the billed session
  }
}

// Task 1: viewport screenshot
export const viewport = (url) => withPage(async (page) => {
  await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 });
  return page.screenshot({ type: 'png' });
});

// Task 2: full page, scrolled first so lazy images load
export const fullPage = (url) => withPage(async (page) => {
  await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 });
  await page.evaluate(async () => {
    for (let y = 0; y < document.body.scrollHeight && y < 16_000; y += innerHeight) {
      scrollTo(0, y);
      await new Promise((r) => setTimeout(r, 400));
    }
    scrollTo(0, 0);
  });
  await page.waitForNetworkIdle({ idleTime: 500, timeout: 10_000 }).catch(() => {});
  return page.screenshot({ type: 'png', fullPage: true });
});

// Task 3: one element
export const element = (url, selector) => withPage(async (page) => {
  await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 });
  const el = await page.waitForSelector(selector, { visible: true, timeout: 10_000 });
  return el.screenshot({ type: 'png' });
});

// Task 4: clean capture (block ad hosts, hide a cookie banner)
export const clean = (url, hide) => withPage(async (page) => {
  await page.setRequestInterception(true);
  page.on('request', (req) => {
    if (req.isInterceptResolutionHandled()) return;
    if (BLOCK.some((re) => re.test(req.url()))) req.abort();
    else req.continue();
  });
  await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 });
  await page.addStyleTag({ content: `${hide.join(',')}{display:none!important}` });
  return page.screenshot({ type: 'png' });
});

// Task 5: A4 PDF with backgrounds
export const pdf = (url) => withPage(async (page) => {
  await page.goto(url, { waitUntil: 'networkidle0', timeout: 30_000 });
  await page.emulateMediaType('print');
  return page.pdf({ format: 'A4', printBackground: true });
});

if (import.meta.url === `file://${process.argv[1]}`) {
  const out = {
    'viewport.png': await viewport(target),
    'full.png': await fullPage(target),
    'element.png': await element(target, 'body p'),
    'clean.png': await clean(target, ['#cookie-banner', '.cookie-notice']),
    'page.pdf': await pdf(target),
  };
  for (const [name, buf] of Object.entries(out)) {
    await writeFile(name, buf);
    console.log(name, buf.length, 'bytes');
  }
}

It is still a simplified version. The blocklist has three hosts, the cookie banner selectors are guesses you must maintain per site, there is no retry, and it opens one browser per task. Production code needs all of those.

Screenshot API version (RenderScreenshot Node SDK)

// The same five tasks with the RenderScreenshot Node SDK.
import { Client, TakeOptions } from 'renderscreenshot';

const client = new Client(process.env.RS_API_KEY);

export const viewport = (url) =>
  client.take(TakeOptions.url(url).width(1280).height(800));

export const fullPage = (url) =>
  client.take(TakeOptions.url(url).width(1280).fullPage());

export const element = (url, selector) =>
  client.take(TakeOptions.url(url).element(selector));

export const clean = (url) =>
  client.take(TakeOptions.url(url).blockAds().blockCookieBanners());

export const pdf = (url) =>
  client.take(TakeOptions.url(url).format('pdf').pdfPaperSize('a4').pdfPrintBackground());

The SDK methods are documented in the Node SDK reference. To scroll for lazy-loaded images on long pages, the POST body has a capture.scroll group, described in the capture options. To hide your own selectors in addition to cookie banners, use the hide parameter from the page modification options.

What we measured

Tested on 2026-10-15 with Node 22.18.0 and [email protected] against https://example.com, on macOS with an Apple M1 Pro. The Puppeteer version ran end to end with local Chrome and produced all five files. The SDK version uses [email protected]; we checked that every option serializes to the documented request fields, but we did not include live API timings here.

Task Puppeteer lines (task only) SDK lines Puppeteer session time, local, median of 3
Viewport 4 2 1.53 s
Full page with scroll 12 2 2.45 s
Element 5 2 1.58 s
Clean capture 11 2 1.55 s
A4 PDF 5 2 1.84 s
Shared setup (imports, connect, cleanup) 18 2
Total 55 12

Lines exclude blank lines, comments and the demo block at the bottom. "Session time" is launch to close for one task, which is the number a time-billed browser service charges you for. On a hosted browser, add the network round trips between your server and the vendor's region for every command; the screenshot API makes one round trip per capture.

The line count is the small part. The 55 lines are the start of a codebase you own: blocklists, banner selectors, timeouts, retries, browser version updates. The 12 lines delegate that to the vendor, and you accept the options the vendor offers.

How the two categories bill

Prices below were read from each vendor's pricing page on 2026-10-14 and 2026-10-15. Check the links before you rely on them.

Vendor Category Billing unit Example plan, as listed in October 2026
Browserless Headless browser API "Unit" = up to 30 seconds of browser time per connection; reconnects start a new unit Prototyping: 20k units, $25/month billed annually, then $0.0020 per unit
Browserbase Headless browser API Browser hours Developer: $20/month with 100 browser hours, then $0.12 per browser hour
Cloudflare Browser Rendering Headless browser API Browser hours, plus concurrent browsers for Puppeteer/Playwright sessions Workers Paid: 10 hours/month included, then $0.09 per hour; 10 concurrent browsers included, then $2.00 per browser
ApiFlash Screenshot API Screenshot Medium: $35/month for 10,000 screenshots
ScreenshotOne Screenshot API Successful screenshot Growth: $79/month for 10,000 screenshots
RenderScreenshot Screenshot API Credit per render; cache hits with cache_available use 0 credits Growth: $40/month for 12,000 credits

Cost per capture, worked out

Take the full-page task: about 2.5 seconds of browser time in our local run. On a hosted browser, assume around 3 seconds once network round trips are added (an assumption, not a measurement).

  • Unit-billed (30-second blocks): one connection per capture uses one unit, whether the capture takes 3 seconds or 29. At $0.0020 per overage unit that is $0.0020 per capture. Keep the connection open and do several captures within 30 seconds, and the cost per capture drops.
  • Hour-billed: 3 seconds is 0.00083 hours. At $0.12 per browser hour that is about $0.0001 per capture, before any per-session minimum or rounding rule the vendor applies. Cloudflare also bills concurrency, which dominates when traffic is bursty.
  • Screenshot API: a flat price per capture. At the RenderScreenshot Growth plan, $40 / 12,000 is about $0.0033 per render; repeat requests served from cache cost nothing.

So raw compute is cheapest on an hour-billed browser. The screenshot API costs more per capture than raw browser time and includes the logic, retries, caching and browser updates. At low volume, the engineering hours to build and maintain the 55-line version (and what it grows into) cost more than the per-capture difference. At high volume with simple pages, the hour-billed browser can win on cost if you invest in the code.

When each one wins

Choose a headless browser API when:

  • The task is a flow: log in, click, wait, then capture. Screenshot APIs can set cookies and headers, and click one selector, but not run arbitrary steps.
  • You scrape or test as well as capture, and want one browser bill.
  • You need features no screenshot API exposes, such as a specific browser extension or a live debugging view of the session.
  • You run AI agents that browse.

Choose a screenshot API when:

  • The input is a URL or HTML and the output is an image or PDF.
  • You want caching, signed URLs for <img> tags, and CDN links without building them. With RenderScreenshot, every response includes an X-Cache-URL header that points to the CDN copy (see response headers).
  • You do not want to track Chrome versions, blocklists and cookie-banner selectors.
  • Your volume is small or spiky, so per-request pricing beats paying for concurrency.

Use both when you have both kinds of work. That is what we do internally: our renderer is code on a headless browser platform, and our customers call it as a screenshot API.

How to decide in five minutes

  1. Write down your task as steps. If any step is "click", "type" or "log in" (beyond setting a cookie), start with a headless browser API.
  2. Otherwise, run your three hardest URLs through a screenshot API and look at the output.
  3. If the output is right, compare the per-capture price at your volume with the time-billed math above, and add the hours you would spend on the 55-line version.

To test step 2 with RenderScreenshot, create a free API key and run the SDK file above with RS_API_KEY set. For vendor-by-vendor lists, see our roundups of the best headless browser APIs and the best screenshot APIs.

Last tested: 2026-10-15. Prices re-checked quarterly.

Frequently asked questions

What is the difference between a screenshot API and a headless browser API?
A screenshot API takes a URL and options and returns an image or PDF. A headless browser API (browser as a service) gives you a remote browser you control with Puppeteer, Playwright or CDP, and you write the capture logic yourself.
Is Browserless a screenshot API?
Browserless is mainly a hosted browser you connect to, but it also offers REST endpoints such as /screenshot. Those endpoints are closer to a screenshot API, while the connect endpoint is a headless browser API.
Which is cheaper per screenshot?
It depends on how the vendor rounds browser time. Per-second browser billing can be cheaper than a per-request screenshot API for short captures, while per-30-second units cost about the same. Add your engineering time to either number.
Can I use both?
Yes. A common split is a screenshot API for URL-to-image jobs and a headless browser API for logins, multi-step flows and scraping.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.