A screenshot API is a web service that loads a URL or a piece of HTML in a hosted browser and returns the rendered result, usually a PNG, JPEG, WebP or PDF, in response to a single HTTP request.
It turns "run a browser" into "call an endpoint". The service owns the headless browsers, the fonts, the wait logic, the caching and the scaling. Your code sends options such as viewport, format and wait condition, and gets bytes or a link back. This entry is part of the screenshot and rendering glossary.
A plain explanation
Taking one screenshot with Puppeteer takes a dozen lines. Taking a million reliably takes a small platform: a browser pool, crash recovery, queues, timeouts, font packages, cookie-banner handling, storage and a cache so the same page is not rendered twice. A screenshot API is that platform, run by someone else, behind a URL.
| Concern | Running your own browser | Screenshot API |
|---|---|---|
| Browser install and updates | You build and patch the image | Provider |
| Concurrency and crashed tabs | Your pool, your restarts | Provider |
| Waiting for SPAs, lazy images, fonts | Your code per site | Request options |
| Cookie banners and ads | Your selectors | Blocking options |
| Storage and CDN links | Your bucket and cache rules | Returned with the image |
| Full control of every step | Yes | Within the option set |
| Cost model | Servers, whether busy or idle | Per capture |
What happens inside one request
When we built RenderScreenshot, we split that path in two. The API application handles everything that is business logic: authentication, validation of every option, credits and the cache lookup. Only a cache miss reaches the browser, which runs on Cloudflare's Browser Rendering platform. The rendered file goes straight to R2 storage, not back through the API servers, and the response carries an X-Cache-URL header with a public CDN link to the same image. See response headers.
The same capture, three ways
Puppeteer, run by you. The smallest version that does not leak browsers.
import puppeteer from 'puppeteer'; import { writeFile } from 'node:fs/promises'; export async function screenshot(url, { width = 1280, height = 800 } = {}) { const browser = await puppeteer.launch(); try { const page = await browser.newPage(); await page.setViewport({ width, height }); await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 }); return await page.screenshot({ type: 'png' }); } finally { await browser.close(); // a leaked browser is the most common DIY outage } } const png = await screenshot(process.argv[2] ?? 'https://example.com'); await writeFile('diy.png', png); console.log(`diy.png ${png.length} bytes`);
Playwright, run by you.
import { chromium } from 'playwright'; import { writeFile } from 'node:fs/promises'; export async function screenshot(url, { width = 1280, height = 800 } = {}) { const browser = await chromium.launch(); try { const page = await browser.newPage({ viewport: { width, height } }); await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 }); return await page.screenshot({ type: 'png' }); } finally { await browser.close(); } } const png = await screenshot(process.argv[2] ?? 'https://example.com'); await writeFile('diy-playwright.png', png); console.log(`diy-playwright.png ${png.length} bytes`);
Both work, and both are the start of the work, not the end. Neither handles concurrency, retries, fonts, banners or storage.
RenderScreenshot. One request. takeJson returns the CDN URL and image details instead of bytes, which is what you store in a database.
import { Client, TakeOptions } from 'renderscreenshot'; const client = new Client(process.env.RS_API_KEY); const result = await client.takeJson( TakeOptions.url('https://example.com').width(1280).height(800).waitFor('networkidle').blockCookieBanners(), ); console.log(result.url, result.width, result.height, result.cached);
curl --get https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ --data-urlencode 'url=https://example.com' \ -d width=1280 -d height=800 -d block_cookies=true \ -D headers.txt --output page.png
The -D flag saves the response headers, so you can read X-Cache-URL and X-Cache-Status from headers.txt.
Tested on 2026-10-15 with Node 22.18, Puppeteer 24.43.1 (run in headless shell mode) and Playwright 1.64.1; the two do-it-yourself functions each wrote a 1280 x 800 PNG of example.com. The RenderScreenshot snippets follow the Node SDK docs and the GET endpoint docs and were not run for this page.
Common mistakes when choosing or using one
- Judging by the demo URL. Every API captures a static page. Test your hardest pages: SPAs, pages with consent banners, long pages with lazy images.
- Rendering on every page view. Cache the result or use the returned CDN link. Re-rendering the same page per visitor wastes time and credits.
- Putting a secret key in public HTML. For image tags and
og:image, use signed URLs. - Ignoring the output format. WebP is often much smaller than PNG for the same capture. Pick per use case.
Related terms
- Headless browser: the engine inside every screenshot API.
- OG image: the most common thing people render through one.
- Model Context Protocol: how AI agents call a screenshot API as a tool.
To compare providers, read how to choose a screenshot API and screenshot API vs headless browser API. For SDKs in your language, see the screenshot API overview. To make your first request, follow the quick start with a free API key. No credit card is needed.