Skip to content

Puppeteer and Playwright screenshots, without running a browser

A Puppeteer or Playwright screenshot script launches Chromium, opens the page, waits, captures and closes the browser. CurlShot does those steps on its own servers: you send one HTTP request with the page address and the same settings as options (viewport, full page, wait condition, PDF paper size) and get the file back. Your code keeps the result and drops the browser, the Docker image, the fonts and the memory limits that come with it.

Request
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com&full_page=true&wait_until=networkidle&block_cookie_banners=true" \
  --output example.png

How to replace a Puppeteer screenshot script with an API call in three steps

  1. 1

    Find the page.screenshot or page.pdf call in your script and note its settings.

  2. 2

    Write the same settings as options of one request: viewport_width, full_page, wait_until and so on.

  3. 3

    Replace the browser launch and the capture with that request, and delete the browser from your build.

The same capture in Puppeteer, Playwright and one request

Each sample below takes a full-page screenshot of a page at 1280 × 1024 after the network has gone quiet. The first two launch a browser on your machine; the last two send the work to the API. The cookie banner is the giveaway: in a script you hide it by hand, site by site, while the API has a switch for the common consent tools.

  • viewport_width=1280 and viewport_height=1024 are the defaults, so the shorter Python request gives the same image.
  • wait_until also accepts networkidle0 and networkidle2, the Puppeteer names, and treats both as networkidle.
Puppeteer
// npm install puppeteer  (downloads a browser of its own)
import puppeteer from 'puppeteer'

const browser = await puppeteer.launch()
try {
  const page = await browser.newPage()
  await page.setViewport({ width: 1280, height: 1024 })
  await page.goto('https://example.com', { waitUntil: 'networkidle0', timeout: 30000 })
  // The cookie banner of this one site, hidden by hand.
  await page.addStyleTag({ content: '#cookie-banner { display: none !important; }' })
  await page.screenshot({ path: 'example.png', fullPage: true })
} finally {
  await browser.close()
}
Playwright (Node.js)
// npm install playwright && npx playwright install chromium
import { chromium } from 'playwright'

const browser = await chromium.launch()
try {
  const page = await browser.newPage({ viewport: { width: 1280, height: 1024 } })
  await page.goto('https://example.com', { waitUntil: 'networkidle', timeout: 30000 })
  await page.addStyleTag({ content: '#cookie-banner { display: none !important; }' })
  await page.screenshot({ path: 'example.png', fullPage: true })
} finally {
  await browser.close()
}
The API (Node.js, no browser)
// Node.js 18 or newer, nothing to install.
import { writeFile } from 'node:fs/promises'

const params = new URLSearchParams({
  url: 'https://example.com',
  viewport_width: '1280',
  viewport_height: '1024',
  wait_until: 'networkidle',
  timeout: '30',
  block_cookie_banners: 'true',
  full_page: 'true',
})
const response = await fetch('https://curlshot.com/api/v1/screenshot?' + params, {
  headers: { 'X-Access-Key': process.env.SCREENSHOT_ACCESS_KEY },
})
if (!response.ok) throw new Error((await response.json()).error_message)
await writeFile('example.png', Buffer.from(await response.arrayBuffer()))
The API (Python, no browser)
# Replaces: sync_playwright() -> chromium.launch() -> page.goto() -> page.screenshot()
import os

import requests

response = requests.get(
    "https://curlshot.com/api/v1/screenshot",
    headers={"X-Access-Key": os.environ["SCREENSHOT_ACCESS_KEY"]},
    params={"url": "https://example.com", "wait_until": "networkidle", "block_cookie_banners": "true", "full_page": "true"},
    timeout=120,
)
response.raise_for_status()
with open("example.png", "wb") as f:
    f.write(response.content)

Puppeteer and Playwright calls, and the option that replaces each

Most of a screenshot script is settings. Each line of the table is one call you can delete, and the option that does the same in the request. Times are in seconds in the API, not milliseconds.

Show the table (25 rows)
PuppeteerPlaywrightAPI option
page.setViewport({ width, height })newPage({ viewport })viewport_width, viewport_height
setViewport({ deviceScaleFactor: 2 })newPage({ deviceScaleFactor: 2 })device_scale_factor=2
page.emulate(KnownDevices[...])newPage({ ...devices[...] })viewport_device, e.g. iphone_15_pro
goto(url, { waitUntil: 'networkidle0' })goto(url, { waitUntil: 'networkidle' })wait_until=networkidle
goto(url, { timeout: 30000 })goto(url, { timeout: 30000 })timeout=30
page.waitForSelector(sel)page.locator(sel).waitFor()wait_for_selector
a sleep of 2000 mspage.waitForTimeout(2000)delay=2
screenshot({ fullPage: true })screenshot({ fullPage: true })full_page=true
screenshot({ type: 'jpeg', quality: 80 })screenshot({ type: 'jpeg', quality: 80 })format=jpeg, image_quality=80
screenshot({ omitBackground: true })screenshot({ omitBackground: true })omit_background=true
screenshot({ clip })screenshot({ clip })clip_x, clip_y, clip_width, clip_height
(await page.$(sel)).screenshot()page.locator(sel).screenshot()selector
page.pdf({ format: 'A4', landscape, margin })page.pdf({ format: 'A4', landscape, margin })format=pdf, pdf_paper_format, pdf_landscape, pdf_margin
pdf({ printBackground: true })pdf({ printBackground: true })pdf_print_background (on by default)
page.emulateMediaType('print')page.emulateMedia({ media: 'print' })media_type=print
emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }])page.emulateMedia({ colorScheme: 'dark' })dark_mode=true
page.setContent(html)page.setContent(html)html, in a POST body
page.setCookie(...)context.addCookies([...])cookies
page.setExtraHTTPHeaders({...})newPage({ extraHTTPHeaders })headers
page.setUserAgent(ua)newPage({ userAgent })user_agent
page.emulateTimezone('Europe/Paris')newPage({ timezoneId: 'Europe/Paris' })time_zone=Europe/Paris
page.addStyleTag({ content })page.addStyleTag({ content })styles, or hide_selectors to hide elements
page.evaluate(fn)page.evaluate(fn)scripts
page.click(sel)page.click(sel)click
setRequestInterception + request.abort()page.route(pattern, (r) => r.abort())block_requests, block_resources

What you stop running when the browser goes

The script above is a dozen lines. What surrounds it in production is not, and none of it is visible until something breaks. This is the work that leaves your servers with the browser.

Show the table (7 rows)
With your own ChromiumWith the API
A build or Docker image with Chromium and its system librariesAn HTTP client
A memory budget per browser, and restarts when one hangs or crashesNot your process
A queue and a cap on parallel pages, sized for your busiest hourYour plan's rate and concurrency limits, with Retry-After
Rules to hide cookie banners and ads, kept current by someoneblock_cookie_banners, block_ads, block_trackers, block_chats
Checks that stop user-supplied URLs from reaching your own networkPrivate addresses refused, redirects included
Chromium updates, and a check that pages still render the same after eachNot part of your build or your releases
Somewhere to keep the files and links that expireresponse_type=json returns a stored file link
  • The costs are real on both sides: the API is a monthly bill, your own browser is servers and the time of the person who keeps it running. The blog article below works through the numbers.

When your own Puppeteer or Playwright is still the right call

An API is not a replacement for a browser automation library. Keep your own when one of these is true.

  • The flow is a session: log in by typing, pass a second factor, fill a form, move across pages, then capture.
  • The page lives on localhost, an intranet or a private network. The API refuses those addresses; for one-off pages you can send the HTML instead.
  • You run end-to-end or visual tests in CI. Playwright Test and its toHaveScreenshot assertion belong next to your code.
  • You need Firefox or WebKit, not only Chromium.
  • Your volume is large and steady enough that one dedicated server, kept busy all month, costs less than a plan.
  • Pages may not leave your infrastructure for legal or contractual reasons.
A full-page capture of the Wikipedia article about the Eiffel Tower, from one request
full_page=true in one request: what fullPage: true gives in Puppeteer, without a browser on your side.

What people use it for

  • A screenshot step in a web app that should not ship Chromium
  • Serverless functions that are too small to run a browser
  • Teams that are tired of keeping a headless browser fleet alive
  • Scripts that only ever open one page and capture it

For developers: one request

The same thing from your own code. It is a plain HTTP request, so it works from any language. Create a free account to get an access key.

Questions people ask

Is there a hosted Puppeteer screenshot API?

CurlShot is one: you send the settings you would pass to page.screenshot or page.pdf as options of an HTTP request, and it runs the browser for you. It is not a remote Puppeteer endpoint you connect to; there is no session to drive, only the capture.

Which browser does it use?

Chromium, the open-source engine behind Chrome: the same engine Puppeteer and Playwright drive by default. Firefox and WebKit are not offered, so keep Playwright for tests that must run in those.

Can I keep my script and only move the screenshot step?

Yes, if the page can be reached from the internet. The API opens the page itself, so it does not share your browser session: pass the cookies or headers the page needs as options.

Are timeouts and delays in milliseconds, as in Puppeteer?

No. timeout and delay are in seconds: timeout=30 is Puppeteer's timeout: 30000. timeout goes up to 90 seconds and delay up to 30.

Can it type into forms or follow several pages?

Only a little: one click on a selector, your own JavaScript before the capture, and cookies or headers for a session. A flow that types, submits and moves across pages is a job for your own Puppeteer or Playwright.

Take as many screenshots as you need

A free account gives you 100 screenshots every month and keeps them in one place. No card needed.