Node.js website screenshots
Take website screenshots in Node.js with Urlbox — signed render links for img tags, sync renders for scripts, and async renders with webhooks.
There are three ways to call Urlbox from Node.js. Pick the one that matches where the screenshot ends up:
- Render link — a signed URL you put straight into an
<img>or<meta>tag. The browser fetches the screenshot; your server never waits on it. - Sync render —
POST /v1/render/syncwaits for the render and returns arenderUrl. Best for scripts, cron jobs and background workers. - Async render —
POST /v1/render/asyncreturns arenderIdimmediately and tells you when the render is done, by webhook or by polling. Best for long or bulk renders.
See Sync vs Async for the trade-offs.
Install
The urlbox NPM package signs render links for you (source on GitHub). The sync and async examples below only need fetch, which is built into Node 18 and later.
npm install urlboxFind your API key and secret in your project's settings on the dashboard. Keep the secret on the server — never ship it to the browser.
Generate a render link
import Urlbox from "urlbox";
const urlbox = Urlbox(process.env.URLBOX_API_KEY, process.env.URLBOX_SECRET);
const imgUrl = urlbox.generateRenderLink({
url: "github.com",
thumb_width: 600,
format: "jpg",
quality: 80,
});
// https://api.urlbox.com/v1/YOUR_API_KEY/TOKEN/jpg?url=github.com&thumb_width=600&quality=80The link is signed with your secret, so nobody can change the options and spend your credits. Render it server-side into your HTML:
<img src={imgUrl} alt="Screenshot of github.com" />The first request renders the page, and later requests for the same link are served from the cache. See Render links for caching and refresh options.
Render synchronously
const response = await fetch("https://api.urlbox.com/v1/render/sync", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.URLBOX_SECRET}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com",
full_page: true,
hide_cookie_banners: true,
}),
});
if (!response.ok) {
throw new Error(`Render failed: ${response.status} ${await response.text()}`);
}
const { renderUrl, size } = await response.json();
console.log(renderUrl, size);renderUrl is a temporary link that expires after 30 days. To keep renders, download the file or have Urlbox save it to your own bucket.
To save the screenshot to disk:
import { writeFile } from "node:fs/promises";
const image = await fetch(renderUrl);
await writeFile("screenshot.png", Buffer.from(await image.arrayBuffer()));Render asynchronously
Start the render with POST /v1/render/async. It returns straight away with a renderId and a statusUrl:
const response = await fetch("https://api.urlbox.com/v1/render/async", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.URLBOX_SECRET}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com",
full_page: true,
// Omit webhook_url if you'd rather poll (option 2 below).
webhook_url: "https://your-app.com/webhooks/urlbox",
}),
});
const { renderId, statusUrl } = await response.json();Then collect the result in one of two ways.
Option 1: receive a webhook
Urlbox posts the result to your webhook_url when the render finishes. Before you trust the payload, check the X-Urlbox-Signature header. It has the form t={timestamp},sha256={signature}, where the signature is an HMAC-SHA256 of {timestamp}.{raw request body} keyed with your project's webhook secret (not your API secret). Here it is in Express:
import crypto from "node:crypto";
import express from "express";
const app = express();
// Keep the raw body: the signature is computed over the exact bytes Urlbox sent.
app.post(
"/webhooks/urlbox",
express.raw({ type: "application/json" }),
(req, res) => {
const header = req.get("x-urlbox-signature") ?? "";
const { t, sha256 } = Object.fromEntries(
header.split(",").map((part) => part.split("=", 2))
);
const expected = crypto
.createHmac("sha256", process.env.URLBOX_WEBHOOK_SECRET)
.update(`${t}.${req.body}`)
.digest("hex");
if (
!sha256 ||
sha256.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(sha256), Buffer.from(expected))
) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
if (event.event === "render.succeeded") {
console.log(event.renderId, event.result.renderUrl);
}
res.sendStatus(200);
}
);The payload carries the renderId, an event such as render.succeeded, and a result with the renderUrl and size. See Webhooks for the full format.
Option 2: poll the status URL
No public endpoint to receive webhooks (a script, a CLI, a local job)? Poll statusUrl until the render finishes:
async function waitForRender(statusUrl, { intervalMs = 2000, timeoutMs = 300_000 } = {}) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const res = await fetch(statusUrl, {
headers: { Authorization: `Bearer ${process.env.URLBOX_SECRET}` },
});
const render = await res.json();
if (render.status === "succeeded") return render; // { renderUrl, size, ... }
if (render.status === "failed") throw new Error(render.reason);
if (render.status === "not-found") throw new Error(`Unknown render ${statusUrl}`);
// "created" or "retrying": still working
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
throw new Error("Timed out waiting for render");
}
const { renderUrl } = await waitForRender(statusUrl);Send your secret with each poll, so only your project can read the render's status. Most renders finish in a few seconds; long full-page renders can take longer, so keep the timeout generous.
Next steps
- Browse every option in the Render Options reference.
- See Common Problems if a screenshot comes back blank, blocked or cut off.
- Read the API reference for every endpoint, error code and response field.