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:
- Viewport screenshot at 1280x800.
- Full-page screenshot, scrolled first so lazy-loaded images appear.
- Screenshot of one element.
- "Clean" capture: ad and tracker hosts blocked, cookie banner hidden.
- 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 anX-Cache-URLheader 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
- 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.
- Otherwise, run your three hardest URLs through a screenshot API and look at the output.
- 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.