Site-Shot

Tutorial ·Apr 5, 2026 ·9 min read

How to Take a Website Screenshot with Node.js

Need to generate website screenshots from your Node.js application? Whether you're building link previews, monitoring dashboards, or visual testing tools, the Site-Shot API turns any URL into an image.

There are two ways to call it, and both are here. Site-Shot ships an official Node.js SDK — npm install site-shot-sdk — a typed client with zero runtime dependencies. If you'd rather not add a dependency at all, the API is one HTTP GET, so the built-in fetch is enough.

Prerequisites

  • Node.js 18 or newer — the SDK requires it, and so does the built-in fetch the dependency-free route uses
  • A Site-Shot API key — API plans start at $5/mo for 2,000 screenshots (pricing). The free browser tool at site-shot.com needs no signup and no key, but it's a browser tool, not an API tier

The Official Node.js SDK

npm install site-shot-sdk
import { SiteShot } from "site-shot-sdk";
import fs from "node:fs/promises";

const client = new SiteShot(process.env.SITESHOT_API_KEY);

const png = await client.capture({ url: "https://example.com/", full_size: true });
await fs.writeFile("screenshot.png", png);

CommonJS works the same way — const { SiteShot } = require("site-shot-sdk");. The package is zero runtime dependencies (it uses the built-in fetch), needs Node.js 18 or newer, and ships TypeScript types alongside both module formats. The target URL is a key in the options object, not a separate argument, and every capture option is a sibling key spelled exactly like the HTTP query parameter. Source and issues live at github.com/site-shot/site-shot-sdk.

Your API key belongs in the constructor or in the SITESHOT_API_KEY environment variable — never hardcoded in a file you commit.

Four Return Modes

One capture concept, four methods — you pick the return type instead of passing a flag:

const png = await client.capture({ url: "https://example.com/" });                       // Buffer
await client.captureToFile({ url: "https://example.com/" }, "shot.png");                 // writes the file
const b64 = await client.captureBase64({ url: "https://example.com/" });                 // string, no data-URL prefix
const meta = await client.captureJson({ url: "https://example.com/", source_code: true }); // parsed envelope

A fifth method, buildUrl(), returns the request URL without sending it — useful when debugging a capture. It embeds your API key as userkey and Site-Shot has no signed-URL scheme, so keep that URL server-side: never put it in an <img src> attribute.

Failures arrive as typed exceptions rather than an error picture pretending to be your screenshot: AuthError, QuotaError, InvalidParamsError, CountryUnavailableError, SiteShotTimeoutError and APIError, all subclasses of SiteShotError and all carrying the HTTP status and raw body. The client does not retry a render that completed with an error — renders cost quota — so its retries option covers connection-level failures only and defaults to 0.

No Dependency: One Plain GET

The API is a single GET request, so the dependency-free route is still first-class:

const fs = require('fs');

async function captureScreenshot(url, outputPath = 'screenshot.png') {
  const params = new URLSearchParams({
    url,
    userkey: process.env.SITESHOT_API_KEY,
    width: '1280',
    height: '1024',
    format: 'png',
  });

  const response = await fetch(`https://api.site-shot.com/?${params.toString()}`);
  if (!response.ok) {
    throw new Error(`API returned ${response.status}`);
  }

  fs.writeFileSync(outputPath, Buffer.from(await response.arrayBuffer()));
}

captureScreenshot('https://example.com');

The API renders the page in a real Chromium browser and returns the image bytes directly. If axios is already your HTTP client, the same request is axios.get('https://api.site-shot.com/', { params, responseType: 'arraybuffer', timeout: 70000 }) — but a dependency you're adding for this alone is better spent on the SDK. Every option the SDK accepts is one of these query parameters, spelled the same way, so the reference table below serves both routes.

Full Page Screenshot

To capture the entire scrollable page rather than just the viewport, ask for full_size and cap the result with max_height (up to 20,000 pixels):

const png = await client.capture({
  url: "https://example.com/",
  full_size: true,
  max_height: 15000,
});
// plain HTTP: add full_size=1 and max_height=15000 to the query string

The API scrolls through the whole document before capturing, which also triggers lazy-loaded images.

Express.js Integration

Serve screenshots dynamically in an Express app — a /preview?url=https://example.com endpoint that returns a thumbnail, useful for link preview cards:

import express from "express";
import { SiteShot } from "site-shot-sdk";

const app = express();
const client = new SiteShot(process.env.SITESHOT_API_KEY);

app.get("/preview", async (req, res) => {
  if (!req.query.url) {
    return res.status(400).send("Missing url parameter");
  }

  try {
    const jpeg = await client.capture({
      url: req.query.url,
      width: 1280,
      height: 800,
      format: "jpeg",
      scaled_width: 400,
    });
    res.set("Content-Type", "image/jpeg");
    res.set("Cache-Control", "public, max-age=3600");
    res.send(jpeg);
  } catch (err) {
    res.status(502).send("Screenshot failed");
  }
});

app.listen(3000);

Cache the result — every request that reaches the API is a render against your quota, and the capture takes seconds, not milliseconds.

Capturing Multiple URLs

const urls = ["https://example.com", "https://wikipedia.org", "https://github.com"];

const files = await Promise.all(
  urls.map(async (url) => {
    const filename = url.replace(/^https?:\/\//, "").replace(/\//g, "_") + ".png";
    await client.captureToFile({ url, width: 1280, height: 1024 }, filename);
    return filename;
  })
);

console.log("Saved:", files);

One client instance is safe to share — it holds no pooled connections. How many of those captures actually run at once depends on your plan's dedicated worker count.

Screenshot from a Specific Country

Pass a two-letter ISO 3166-1 country code and the API routes the render through an IP in that country, with a matching language, time zone, and geolocation:

import { SiteShot, CountryUnavailableError } from "site-shot-sdk";

const client = new SiteShot(process.env.SITESHOT_API_KEY, { retries: 2 });

try {
  const png = await client.capture({
    url: "https://whatismycountry.com/",
    country: "DE",
    strict_country: true,
    no_ads: true,
    no_cookie_popup: true,
  });
} catch (err) {
  if (err instanceof CountryUnavailableError) {
    // no capacity in DE right now — retry later, or drop strict_country
  }
}

Use ISO codes only — country: "DE", not the country's full name, which is not a valid value and silently renders from the US instead. The same silent US fallback happens when the requested country has no capacity at that moment; strict_country turns that into an honest failure — country_unavailable in the API's JSON, CountryUnavailableError in the SDK. Country routing is included with any paid plan.

JSON, Metadata and Rendered HTML

When you need more than the image — the target's HTTP status, its response headers, or the HTML as rendered — ask for the JSON result:

const meta = await client.captureJson({ url: "https://example.com/", source_code: true });

console.log(meta.response.status_code);   // 200
console.log(meta.response.headers);       // [{ name: ..., value: ... }, ...]
console.log(meta.source_code.slice(0, 200)); // rendered HTML

On the plain-HTTP route the same envelope comes back from response_type=json, and the image arrives as a data URL you strip and decode yourself:

const params = new URLSearchParams({
  url: 'https://example.com',
  userkey: process.env.SITESHOT_API_KEY,
  response_type: 'json',
});

const payload = await (await fetch(`https://api.site-shot.com/?${params.toString()}`)).json();
if (payload.error) {
  throw new Error(payload.error);
}

const base64 = payload.image.split(',').pop();
fs.writeFileSync('screenshot.png', Buffer.from(base64, 'base64'));

The envelope carries image (a base64 data URL), response (the target's status_code and headers), screenshot_parameters (every value the renderer actually used), and source_code when you ask for it. A failed capture arrives in-band as a JSON body with an error field — country_unavailable, for instance — which is why that check comes before the decode. The SDK runs the check for you and throws the matching typed error instead.

Where the honest limits are

  • Two languages have an SDK. Node.js (npm install site-shot-sdk) and Python (pip install site-shot). In Go, Ruby, Java, C# or PHP you call the same HTTP endpoint directly — the plain-GET section above is the whole integration.
  • Node.js 18 or newer. The SDK is built on the runtime's own fetch; on Node 16 and older you are on the plain-HTTP route with a fetch polyfill or axios.
  • The SDK sends GET requests. A very long javascript_code or user_agent value can push the URL past practical length limits of about 8 KB.
  • The API always needs a key. The no-signup path is the browser tool, not the API.
  • Images only. Site-Shot returns PNG or JPEG — no PDF and no video output.

Parameter Reference

The SDK takes booleans where the query string takes 1/0. Unknown options pass straight through, so a new API parameter works without an SDK release.

Parameter Description Example
url Target web page (a key in the SDK's options object) https://example.com
userkey Your API key (SDK: constructor argument or SITESHOT_API_KEY) abc123
width Viewport width (100–8000) 1280
height Viewport height (100–20000) 1024
full_size Capture full page true / 1
max_height Height cap for full-page captures 15000
format Output format png or jpeg
scaled_width Resize output image 400
delay_time Wait before capture (ms) 2000
response_type Response format (SDK: pick the method) image or json
source_code Include the rendered HTML true / 1
country Proxy country (two-letter ISO code) DE
strict_country Fail instead of falling back to the US true / 1
no_ads / no_cookie_popup Strip ads / cookie banners true / 1

Check the full API documentation for all available parameters. For scheduled captures without code, see automatic daily screenshots; for the viewport-vs-full-page decision, see full page vs viewport screenshots; for country routing in depth, see screenshots from another country.

FAQ

Is there an official Node.js SDK for Site-Shot?

Yes. npm install site-shot-sdk installs the official client: zero runtime dependencies, Node.js 18 and newer, TypeScript types bundled, ESM and CommonJS. It wraps the same HTTP API — client.capture({ url, full_size: true }) returns a Buffer of PNG bytes, and captureToFile(), captureBase64() and captureJson() cover the other return modes. Python has an official SDK too (pip install site-shot); in every other language you call the HTTP API directly.

Do I need an API key to take website screenshots with Node.js?

For the Site-Shot API, yes — every request carries your userkey (the SDK takes it in the constructor, or reads SITESHOT_API_KEY from the environment, and sends it for you), and keys come with any paid plan (from $5/mo for 2,000 screenshots). For a quick one-off capture without code, the free in-browser tool at site-shot.com needs no signup and no key at all.

How do I capture a full-page screenshot in Node.js?

Pass full_size: true to the SDK, or add full_size=1 to a plain HTTP request, and cap the height with max_height (up to 20,000 pixels). The API scrolls the whole document — triggering lazy-loaded content — and returns one tall image.

Can I schedule these captures to run automatically?

Yes, two ways: run your Node.js script from cron or CI, or skip the code entirely — Site-Shot's built-in scheduled captures re-photograph a URL on a cadence you choose and file every shot in your library, included with any paid plan. See automatic daily screenshots.

Capture your first screenshot free in your browser — no signup — at site-shot.com, or compare API plans on the pricing page.

← All articles