· Updated · Engineering · By Simon Chiu

RenderScreenshot Node.js SDK: Install and Tested Examples

Install the renderscreenshot npm package and take screenshots with Client and TakeOptions. Tested examples, error handling and the known issues in 1.1.0.

Line illustration of the RenderScreenshot mascot unboxing a Node.js module connected to a browser

The official Node.js SDK for RenderScreenshot is the renderscreenshot package on npm. It wraps the screenshot API in a Client class and a fluent TakeOptions builder, with TypeScript types included. This post shows how to install it, five examples we tested against the current release, and the known issues in version 1.1.0 with a workaround for each. For the full method list, see the Node.js SDK reference; for a framework-by-framework overview, see the Node.js screenshot API page.

Tested on October 15, 2026 with Node.js 22.18.0 and [email protected], against a local mock of POST /v1/screenshot that returns the API's real response shapes. Each example's request body was checked against the parameters the API accepts.

Install

npm install [email protected]

The package needs Node.js 20 or later. It ships ESM and CommonJS builds and its own type definitions. Keep your API key in an environment variable; all examples read RS_API_KEY.

1. Take a screenshot

// og-card.mjs
import { writeFile } from 'node:fs/promises';
import { Client, TakeOptions } from 'renderscreenshot';

const client = new Client(process.env.RS_API_KEY);

const image = await client.take(
  TakeOptions.url('https://example.com').preset('og_card'),
);
await writeFile('og-card.png', image);

take() returns a Buffer with the image. The og_card preset renders a 1200x630 PNG; the other presets and their sizes are in the presets reference.

2. Set the viewport or emulate a device

// Retina desktop capture: 1440x900 viewport at device scale factor 2
const desktop = await client.take(
  TakeOptions.url('https://example.com').width(1440).height(900).scale(2),
);

// Phone capture with a device preset
const phone = await client.take(
  TakeOptions.url('https://example.com').device('iphone_14_pro'),
);

width, height, scale and mobile are sent in the nested viewport object, and device at the top level, which is what the API expects.

3. Handle errors by HTTP status

import { Client, TakeOptions, RenderScreenshotError } from 'renderscreenshot';

const client = new Client(process.env.RS_API_KEY);

try {
  const image = await client.take(TakeOptions.url('https://example.com'));
  console.log(`Got ${image.length} bytes`);
} catch (err) {
  if (!(err instanceof RenderScreenshotError)) throw err;
  if (err.httpStatus === 429) {
    console.error(`Rate limited. Retry in ${err.retryAfter ?? 60} s.`);
  } else if (err.httpStatus === 401 || err.httpStatus === 403) {
    console.error('Check RS_API_KEY.');
  } else {
    console.error(`Request failed with HTTP ${err.httpStatus}.`);
  }
  process.exitCode = 1;
}

Branch on err.httpStatus, not err.code, in 1.1.0 (see the known issues below). retryAfter comes from the Retry-After header and is correct. Our load testing guide shows what rate limits look like under traffic.

4. Options the 1.1.0 builder does not send correctly

For full-page captures, output format, wait conditions and blocking, call the API with fetch and the nested JSON body from the POST endpoint reference until the SDK fix ships:

// full-page.mjs
import { writeFile } from 'node:fs/promises';

const res = await fetch('https://api.renderscreenshot.com/v1/screenshot', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.RS_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://example.com',
    capture: { mode: 'full_page' },
    output: { format: 'webp', quality: 80 },
    wait: { until: 'networkidle' },
    block: { cookie_banners: true, ads: true },
  }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);

await writeFile('full-page.webp', Buffer.from(await res.arrayBuffer()));
console.log('CDN copy:', res.headers.get('x-cache-url'));

The X-Cache-URL header holds a public CDN link to the same image, valid until the cache expires. The other response headers are listed in the headers reference.

5. Signed URLs for img and og:image tags

To put a screenshot URL in public HTML, sign it on your server with your rs_pub_ key ID and rs_secret_ secret. Use the signing function from the signed URL documentation, which is tested against the API's verifier:

import { createHmac } from 'node:crypto';

function encode(value) {
  return encodeURIComponent(String(value))
    .replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
    .replace(/%20/g, '+');
}

export function signedScreenshotUrl(params, { keyId, secret, expiresAt }) {
  const fields = { ...params, key_id: keyId, expires: Math.floor(expiresAt.getTime() / 1000) };
  const query = Object.entries(fields)
    .map(([k, v]) => `${encode(k)}=${encode(v)}`)
    .sort()
    .join('&');
  const signature = createHmac('sha256', secret).update(query).digest('hex');
  return `https://api.renderscreenshot.com/v1/screenshot?${query}&signature=${signature}`;
}

Expiry is required, with a maximum of 30 days. Do not use client.generateUrl() from 1.1.0 for this; see below.

Known issues in 1.1.0

We found these while writing the tests for this post. Each one is reproducible with a mock server that records what the SDK sends.

Area What 1.1.0 does Effect Use instead
Builder options beyond the viewport Sends full_page, format, quality, wait_for, block_*, dark_mode and similar as flat top-level keys in the POST body The POST endpoint reads these from nested objects (capture, output, wait, block, browser), so they are ignored and you get the defaults fetch with the nested body (example 4)
takeJson() Adds response_type: 'json' to the body The API returns JSON only when the request has an Accept: application/json header, so the call receives an image and fails to parse it fetch with Accept: application/json
generateUrl() Signs with the client's own key, omits key_id, and does not form-encode values The URL does not pass signature verification The signing function in example 5
Error codes Reads code from the top of the error body, but the API nests it under error err.code is internal_error and err.retryable is true for every API error err.httpStatus and err.retryAfter (example 3)
presets() Typed as an array The API returns { "presets": [...] } Read the presets property

What works as documented in 1.1.0: the Client constructor and its baseUrl and timeout options, take() with url, html, preset, device, width, height, scale and mobile, and the httpStatus and retryAfter fields of RenderScreenshotError.

A fixed release is the next item on the SDK's list. This post will be updated with the version number, and the issues table will go away, when it ships.

How the SDK fits the architecture

The SDK is a thin HTTPS client. Each take() is one POST /v1/screenshot to our Rails API, which checks the key, expands presets, looks up the cache and counts credits, then asks a Cloudflare Worker to drive the browser. The image lands in R2 and comes back in the response. The full path, with the design decisions behind it, is in how we built RenderScreenshot.

Get a key

The examples above need an API key. Create a free account, copy the rs_live_ key from the dashboard into RS_API_KEY, and run example 1.

Related in this topic