Features Full Page Screenshot Wait for Selector & Delay Block Cookie Banners Custom Viewport & Device Website to PDF HTML to Image Dark Mode Image Format & Quality MCP Server Pricing Docs Blog Log In Sign Up

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:

ParameterTypeDefaultDescription
urlstringThe webpage to capture
formatstringpngOutput: png, jpeg, webp, avif, pdf
widthinteger1280Viewport width in pixels
heightinteger720Viewport height in pixels
full_pagebooleanfalseCapture the entire scrollable page
devicestringdesktopDevice type: desktop, mobile, tablet
retinabooleanfalseRender at 2x pixel density
dark_modebooleanfalseForce dark color scheme
selectorstringCSS selector to capture a specific element
delayinteger0Wait N milliseconds before capturing
block_cookiesbooleanfalseRemove cookie consent banners
block_adsbooleanfalseRemove advertisements
block_chatsbooleanfalseRemove live chat widgets
cssstringInject custom CSS before capture
jsstringExecute 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
SetupInstall Chromium (170-400 MB), configure Docker, manage fontsOne HTTP GET request
Dependenciespuppeteer or playwright + Chromium binaryNone (native fetch)
Memory usageEach headless tab adds a few hundred MB to your footprintZero on your server
ScalingWorker pools, queue management, OOM monitoringHandled by the API
Cookie bannersCustom selectors per site, ongoing maintenanceOne parameter: block_cookies=true
Font renderingInstall font packages on every serverConsistent across all captures
CostServer CPU/RAM + engineering hoursFree 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

Frequently asked questions

No. The API is a standard HTTP endpoint. Node.js 18 and newer include fetch() natively, so you can call it without any third-party packages. If you prefer axios or got, those work too, but nothing extra is required.
Any version that can make HTTP requests. The examples on this page use fetch(), which requires Node 18+. If you are on an older version, use the built-in https module or axios. The API itself is version-agnostic because it is just an HTTP call.
A typical capture takes 2-5 seconds depending on the target page complexity. That is comparable to Puppeteer on a well-configured server. The difference shows at scale: the API handles concurrency and queuing for you, while a local Puppeteer setup needs worker management and enough RAM for parallel browser instances.
Yes. The API runs a full Chromium browser, so JavaScript-heavy pages (React, Vue, Angular, SPAs) render completely. You can add a delay parameter for pages that load data asynchronously, or use wait_for_selector to wait until a specific element appears before capturing.
The free plan includes 200 screenshots per month. There is also a per-second rate limit to prevent abuse. If you hit a rate limit, the API returns a 429 status with a Retry-After header. The retry function shown above handles this automatically.