Node.js Screenshot API — Capture Websites with One Request
Running headless Chrome in a Node.js app works fine on your laptop. Then you deploy to production, and the problems start piling up. Chromium processes consume a few hundred megabytes of RAM each. Docker images swell past a gigabyte. Fonts look wrong on Ubuntu. Timeouts from third-party scripts turn a ten-line capture function into something you dread debugging.
The ScreenshotRun Node.js screenshot API reduces that to a single GET request. Pass a URL, receive a screenshot. PNG, JPEG, WebP, AVIF, or PDF. It works with the native fetch() that ships with Node 18+ and requires zero npm packages.
Your first screenshot in 30 seconds
Grab an API key from your dashboard (the free plan includes 200 captures per month, no credit card). Then run this:
const fs = require('fs');
const params = new URLSearchParams({
url: 'https://github.com',
response_type: 'image'
});
const response = await fetch(
`https://api.screenshotrun.com/v1/screenshots/capture?${params}`,
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } }
);
const buffer = Buffer.from(await response.arrayBuffer());
fs.writeFileSync('github.png', buffer);
console.log('Saved: github.png');
That is the entire integration. No Chromium download, no puppeteer in your node_modules, no browser lifecycle to manage. The API launches a real Chrome instance on our side, waits for the page to finish rendering, and sends back the result.
Full example with fetch (Node.js 18+)
Node 18 shipped with a built-in fetch() implementation, so you can call the Node.js screenshot API without adding dependencies. Here is a more complete version with error handling and configurable output:
const fs = require('fs');
async function captureScreenshot(targetUrl, options = {}) {
const params = new URLSearchParams({
url: targetUrl,
format: options.format || 'png',
width: options.width || '1280',
height: options.height || '720',
full_page: options.fullPage ? 'true' : 'false',
response_type: 'image'
});
const response = await fetch(
`https://api.screenshotrun.com/v1/screenshots/capture?${params}`,
{ headers: { Authorization: `Bearer ${process.env.SCREENSHOTRUN_KEY}` } }
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Screenshot failed: ${error.message}`);
}
const buffer = Buffer.from(await response.arrayBuffer());
return buffer;
}
// Usage
const screenshot = await captureScreenshot('https://github.com', {
format: 'webp',
width: 1920,
fullPage: true
});
fs.writeFileSync('github-full.webp', screenshot);
console.log('Captured full-page WebP screenshot');
Keep your API key in an environment variable (SCREENSHOTRUN_KEY) instead of hardcoding it. This keeps credentials out of your repository and lets you rotate keys without touching code.
Using axios instead of fetch
If your project already uses axios for HTTP calls, the integration looks similar. The main difference is how you handle the binary response:
const axios = require('axios');
const fs = require('fs');
async function captureScreenshot(targetUrl, options = {}) {
const { data } = await axios.get(
'https://api.screenshotrun.com/v1/screenshots/capture',
{
params: {
url: targetUrl,
format: options.format || 'png',
width: options.width || 1280,
height: options.height || 720,
full_page: options.fullPage || false,
response_type: 'image'
},
headers: { Authorization: `Bearer ${process.env.SCREENSHOTRUN_KEY}` },
responseType: 'arraybuffer'
}
);
return Buffer.from(data);
}
const screenshot = await captureScreenshot('https://stripe.com', {
format: 'jpeg',
width: 1440,
fullPage: true
});
fs.writeFileSync('stripe.jpeg', screenshot);
Set responseType: 'arraybuffer' so axios returns raw bytes instead of trying to parse the image as JSON. This is the most common gotcha when integrating binary APIs with axios.
Available parameters
Every parameter goes into the query string of the GET request. The table below covers what you will reach for most often:
| Parameter | Type | Default | Description |
|---|---|---|---|
url | string | — | The webpage to capture |
format | string | png | Output: png, jpeg, webp, avif, pdf |
width | integer | 1280 | Viewport width in pixels |
height | integer | 720 | Viewport height in pixels |
full_page | boolean | false | Capture the entire scrollable page |
device | string | desktop | Device type: desktop, mobile, tablet |
retina | boolean | false | Render at 2x pixel density |
dark_mode | boolean | false | Force dark color scheme |
selector | string | — | CSS selector to capture a specific element |
delay | integer | 0 | Wait N milliseconds before capturing |
block_cookies | boolean | false | Remove cookie consent banners |
block_ads | boolean | false | Remove advertisements |
block_chats | boolean | false | Remove live chat widgets |
css | string | — | Inject custom CSS before capture |
js | string | — | Execute JavaScript before capture |
The full parameter reference lives in the API documentation. What you see above handles 90% of production screenshot tasks.
Dark mode, element capture, and CSS injection
Three capabilities that show up in almost every production integration:
Dark mode. Append dark_mode=true and the API sets prefers-color-scheme: dark before loading the page. Any site that respects this media query will switch to its dark theme automatically:
const params = new URLSearchParams({
url: 'https://github.com',
dark_mode: 'true',
response_type: 'image'
});
Element-level capture. Target a specific component with a CSS selector instead of capturing the whole viewport. The API clips the output to that element's bounding box:
const params = new URLSearchParams({
url: 'https://github.com/anthropics',
selector: '.js-repo-list',
response_type: 'image'
});
CSS injection. Need to hide a promo bar or swap a font before capture? Pass raw CSS through the css parameter:
const params = new URLSearchParams({
url: 'https://example.com',
css: '.promo-banner { display: none !important; } body { font-family: Inter, sans-serif; }',
response_type: 'image'
});
These parameters combine freely. A single request can capture one element, in dark mode, with injected styles.
Handling errors in production
API calls fail. Networks go down, target sites return errors, rate limits apply. A production integration needs retry logic:
async function captureWithRetry(url, options = {}, maxRetries = 2) {
const params = new URLSearchParams({
url,
format: options.format || 'png',
response_type: 'image',
...options.params
});
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(
`https://api.screenshotrun.com/v1/screenshots/capture?${params}`,
{ headers: { Authorization: `Bearer ${process.env.SCREENSHOTRUN_KEY}` } }
);
if (response.ok) {
return Buffer.from(await response.arrayBuffer());
}
const body = await response.json().catch(() => ({}));
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('retry-after') || '5', 10);
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
if (response.status >= 500 && attempt < maxRetries) {
await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
continue;
}
throw new Error(`Screenshot API error ${response.status}: ${body.message || 'Unknown error'}`);
}
}
The function retries on rate limits (429) and server errors (5xx) with progressive backoff, but stops immediately on client errors like invalid parameters or bad credentials. In production, pipe these failures into whatever monitoring you already have running.
Express.js proxy route
A pattern that comes up regularly: your frontend needs screenshots, and you want to proxy requests through your backend so the API key stays server-side.
const express = require('express');
const app = express();
app.get('/api/screenshot', async (req, res) => {
const { url, format = 'png', width = '1280' } = req.query;
if (!url) {
return res.status(400).json({ error: 'Missing url parameter' });
}
const params = new URLSearchParams({
url,
format,
width,
response_type: 'image'
});
const response = await fetch(
`https://api.screenshotrun.com/v1/screenshots/capture?${params}`,
{ headers: { Authorization: `Bearer ${process.env.SCREENSHOTRUN_KEY}` } }
);
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Capture failed' }));
return res.status(response.status).json(error);
}
const contentType = response.headers.get('content-type');
res.setHeader('Content-Type', contentType);
res.setHeader('Cache-Control', 'public, max-age=3600');
const buffer = Buffer.from(await response.arrayBuffer());
res.send(buffer);
});
app.listen(3000);
The endpoint takes a URL from your client, forwards it to ScreenshotRun with your server-side key, and pipes the image back. The client never touches the API key. The Cache-Control header prevents repeated API calls for the same URL within the cache window.
API vs. self-hosted Puppeteer and Playwright
Puppeteer and Playwright are solid choices for browser automation. If your workflow involves filling forms, navigating multi-step flows, or running end-to-end tests, they are the right tool. But when the job is converting URLs into images at scale, the operational overhead stacks up.
| Self-hosted (Puppeteer/Playwright) | ScreenshotRun API | |
|---|---|---|
| Setup | Install Chromium (170-400 MB), configure Docker, manage fonts | One HTTP GET request |
| Dependencies | puppeteer or playwright + Chromium binary | None (native fetch) |
| Memory usage | Each headless tab adds a few hundred MB to your footprint | Zero on your server |
| Scaling | Worker pools, queue management, OOM monitoring | Handled by the API |
| Cookie banners | Custom selectors per site, ongoing maintenance | One parameter: block_cookies=true |
| Font rendering | Install font packages on every server | Consistent across all captures |
| Cost | Server CPU/RAM + engineering hours | Free tier (200/month), then usage-based |
If your app needs a real browser for clicking through forms and navigating flows, keep Puppeteer or Playwright. If the job is turning URLs into images without managing infrastructure, an API call means less code and fewer things that can break at 3 AM.
Start capturing screenshots from Node.js
Create a free account, grab your API key, and drop it into any of the examples above. Two hundred screenshots per month on the free plan, no credit card.
Get Your Free API Key