Node.js Screenshot API: Puppeteer, Playwright, and Hosted APIs
Build a Node.js screenshot API with Puppeteer or Playwright, compare hosted services, handle full-page and element captures, and choose the right setup.
Direct answer: In Node.js, you can capture screenshots by running a browser with Puppeteer or Playwright, navigating to a URL, waiting for the page to reach the required state, and calling page.screenshot(). Use a hosted screenshot API when you want to avoid operating Chromium, queues, storage, retries, and scaling yourself. For production work, decide first whether you need browser-level control or an HTTP endpoint.
Choose an approach
| Approach | Best for | What you operate |
|---|---|---|
| Puppeteer | Chrome-focused Node.js automation and direct browser control | Chromium, concurrency, queues, storage, retries, observability |
| Playwright | Cross-browser capture and broader automation APIs | Browser binaries, contexts, workers, queues, storage |
| Hosted screenshot API | A simple HTTP integration without browser infrastructure | Authentication, request validation, retries, quota handling |
Puppeteer documents Page.screenshot() and element screenshots in its API reference. Playwright exposes the same page-level screenshot pattern across Chromium, Firefox, and WebKit. See the Puppeteer screenshot API and Playwright page screenshot API.
Build a screenshot endpoint with Puppeteer
1. Create the project
mkdir node-screenshot-api
cd node-screenshot-api
npm init -y
npm install express puppeteer
Puppeteer downloads a compatible browser during installation. In a deployment image, make sure the browser and its system dependencies are available.
2. Add an HTTP endpoint
const express = require('express');
const puppeteer = require('puppeteer');
const app = express();
app.use(express.json());
let browserPromise;
function getBrowser() {
if (!browserPromise) {
browserPromise = puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
}
return browserPromise;
}
app.post('/screenshot', async (req, res) => {
const { url, fullPage = true, selector, width = 1440, height = 900 } = req.body;
if (typeof url !== 'string' || !/^https?:\/\//i.test(url)) {
return res.status(400).json({ error: 'url must be an http or https URL' });
}
const browser = await getBrowser();
const page = await browser.newPage();
try {
await page.setViewport({ width, height, deviceScaleFactor: 1 });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 45000 });
const screenshotOptions = {
path: 'screenshot.png',
type: 'png',
fullPage
};
if (selector) {
const element = await page.waitForSelector(selector, { timeout: 10000 });
if (!element) return res.status(422).json({ error: 'selector not found' });
await element.screenshot({ path: 'screenshot.png', type: 'png' });
} else {
await page.screenshot(screenshotOptions);
}
res.sendFile(require('path').resolve('screenshot.png'));
} catch (error) {
res.status(502).json({ error: error.message });
} finally {
await page.close();
}
});
const server = app.listen(3000, () => {
console.log('Screenshot API listening on http://localhost:3000');
});
async function shutdown() {
if (browserPromise) (await browserPromise).close();
server.close(() => process.exit(0));
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
Run it with node server.js, then call it:
curl -X POST http://localhost:3000/screenshot \
-H 'content-type: application/json' \
-d '{"url":"https://example.com","fullPage":true}' \
-o screenshot.png
Puppeteer capture options
page.screenshot() accepts options for the output and the region:
| Option | Use |
|---|---|
path |
Write the image to a file. |
type |
png by default; use jpeg or webp where supported. |
quality |
JPEG/WebP quality; it is ignored for PNG. |
fullPage |
Capture the complete scrollable page instead of the viewport. |
clip |
Capture a specific rectangle with x, y, width, and height. |
encoding |
Return binary data or base64. |
omitBackground |
Preserve transparency where the page background permits it. |
Full-page capture
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'full-page.png', fullPage: true });
Long pages can create very large images. Use a sensible viewport, image format, and maximum page height in your service. For pages that lazy-load images, scroll before capture so content has a chance to render.
Element capture
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
const card = await page.waitForSelector('.pricing-card', { timeout: 10000 });
if (!card) throw new Error('pricing card not found');
await card.screenshot({ path: 'pricing-card.png' });
Puppeteer attempts to scroll an element into view before taking its screenshot. A missing or hidden selector should be treated as a client error, not retried indefinitely.
Wait for application state
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-rendered="true"]', { timeout: 15000 });
await new Promise(resolve => setTimeout(resolve, 500));
await page.screenshot({ path: 'ready.png', fullPage: true });
Use load when page resources must finish, domcontentloaded for an earlier DOM milestone, or networkidle2 when the application settles. A fixed delay alone is fragile; combine it with a selector or application signal.
Hide or modify content
await page.addStyleTag({
content: '.cookie-banner, .chat-widget, .newsletter-modal { display: none !important; }'
});
await page.screenshot({ path: 'clean.png', fullPage: true });
You can also run JavaScript before capture:
await page.evaluate(() => {
document.querySelectorAll('.ads, .cookie-banner').forEach(node => node.remove());
});
Playwright alternative
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1
});
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'playwright.png', fullPage: true });
await browser.close();
})();
Replace chromium with firefox or webkit when cross-browser rendering matters. Choose Playwright when those engines or its wider testing and automation surface are part of the requirement; choose Puppeteer when a Chrome-focused workflow is the better fit.
Hosted screenshot APIs
A hosted service accepts a URL and capture settings over HTTP, then returns an image, PDF, redirect, or URL depending on the provider. The researched Screenshot API documents GET and POST requests, API-key authentication, PNG/JPEG/WebP/PDF output, viewport and full-page settings, device scale factor, wait strategies, selector capture, selector waits, delays, ad and cookie-banner blocking, dark mode, hidden selectors, injected CSS and JavaScript, geolocation, timezone, locale, PDF options, caching, cache TTL, stale TTL, navigation timeout, redirects, batch jobs, polling, and server-sent events.
Its documented errors include 401 unauthorized, 400 invalid request, 429 rate limit or quota exceeded, 502 render failure, and 422 selector not found. The published quota is 60 requests per minute and 500 screenshots per month for the cited 2026 plan; verify current limits and pricing before relying on them.
For a hosted API, send authentication in a header when possible, validate URLs before forwarding them, set a client timeout longer than the provider’s navigation timeout, and persist the provider’s job ID or returned URL for retries and auditing.
Production design checklist
- Reuse a browser process, but create a fresh page or context for each request.
- Set navigation and total request timeouts.
- Limit concurrent pages to protect memory and CPU.
- Validate schemes and block access to internal network ranges if users can submit arbitrary URLs.
- Use a queue for burst traffic and return a job ID for long captures.
- Store output outside the application container when files must survive restarts.
- Record URL, options, duration, browser errors, output size, and retry count.
- Close pages in a
finallyblock and close the browser on process shutdown. - Use deterministic fonts, locale, timezone, and viewport when image diffs matter.
Performance, reliability, and cost
Browser startup is expensive compared with opening a page in an existing browser, so keep one browser process warm and cap page concurrency. Full-page images, high device scale factors, animations, videos, and third-party scripts increase memory, transfer size, and capture time. Disable unnecessary resources only when doing so does not change the page you need to represent.
Retries should distinguish transient navigation failures from deterministic errors. Retry timeouts and temporary upstream failures with exponential backoff and a maximum attempt count. Do not retry invalid URLs, missing selectors, authentication failures, or quota errors without changing the request or account state.
Self-hosting has no per-shot vendor charge, but your application owns browser binaries, compute, storage, bandwidth, maintenance, and operational work. A hosted API turns those into request costs and quotas. The cited research does not establish a benchmark or reliability SLA, so measure your own URLs and workload before selecting an architecture.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Blank or incomplete screenshot | Capture happened before client rendering finished. | Wait for a stable selector, application-ready signal, or appropriate network state. |
| Lazy images are missing | Images load only after scrolling. | Scroll through the page before capture or use a service that loads lazy images. |
| Element screenshot fails | Selector is absent, hidden, or inside a frame. | Wait for the selector, check visibility, and switch to the correct frame. |
| Navigation timeout | Slow origin, blocked request, or never-ending network activity. | Increase the timeout carefully, use a less strict wait condition, and inspect failed requests. |
| Fonts differ in production | Font files are unavailable or load at different times. | Install required fonts, wait for document.fonts.ready, and pin locale and rendering inputs. |
| Large memory usage | Too many pages, huge full-page images, or leaked contexts. | Cap concurrency, close every page, and reject unreasonable dimensions. |
| Hosted API returns 401 | Missing or invalid API key. | Send the key using the provider’s documented authentication method. |
| Hosted API returns 429 | Rate or monthly quota exceeded. | Back off, queue work, and check account limits. |
| Hosted API returns 422 | Requested selector was not found. | Confirm the selector against the final DOM and increase the selector wait only when appropriate. |
Or skip the browser setup
ScreenshotNeo is a hosted website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. It accepts 63 capture options, including full-page screenshots with lazy images loaded, CSS-selector element capture, dark mode, device presets, arbitrary viewports, retina scale, PDF paper and page controls, custom CSS and JavaScript, click actions, hidden selectors, selector or network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous jobs, signed webhooks, bulk capture of up to 100 URLs, usage reporting, and an OpenAPI specification. The parameter names used by other screenshot APIs also work.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const image = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', image);
See the ScreenshotNeo API documentation for request options. Cookie and consent banners are accepted and removed before capture, along with more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
FAQ
Can Node.js take screenshots without Puppeteer?
Yes. Use Playwright or a hosted HTTP screenshot API. Puppeteer and Playwright are browser automation libraries; a hosted API moves browser operations out of your application.
Should I use PNG, JPEG, or WebP?
Use PNG for lossless UI images and transparency, JPEG for photographic content, and WebP when supported clients benefit from smaller files.
How do I capture a PDF?
Use the browser’s PDF API for self-hosted Chromium or a hosted service that documents PDF output and paper, margin, orientation, and page-range settings.
Is a fixed delay enough?
No. Prefer a selector or application-ready signal, then add a short delay only for animations or late visual transitions.


