How to Build a Screenshot API in Node.js
If multiple services in your stack need screenshots, the cleanest pattern is a single internal API they all call. One place to manage the key, one place to add caching, one place to swap providers.
If multiple services in your stack need screenshots — your dashboard, your monitoring tool, your email previews — the cleanest pattern is a single internal screenshot API that all of them call. One place to manage the API key, one place to add caching, one place to change providers.
Here's how to build it in Node.js in about 50 lines.
Basic Express API
import express from 'express';
import fetch from 'node-fetch';
const app = express();
app.use(express.json());
app.post('/screenshot', async (req, res) => {
const { url, html, fullPage = true, format = 'png' } = req.body;
if (!url && !html) {
return res.status(400).json({ error: 'url or html required' });
}
const capture = await fetch('https://pagebolt.dev/api/v1/screenshot', {
method: 'POST',
headers: {
'x-api-key': process.env.PAGEBOLT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ url, html, fullPage, blockBanners: true, format })
});
if (!capture.ok) {
return res.status(502).json({ error: 'capture failed' });
}
const image = Buffer.from(await capture.arrayBuffer());
res.setHeader('Content-Type', `image/${format}`);
res.send(image);
});
app.listen(3000);
Add simple in-memory caching
Screenshots of the same URL rarely change within a few minutes. Cache by URL to avoid redundant captures:
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({ max: 200, ttl: 5 * 60 * 1000 }); // 5 min TTL
app.post('/screenshot', async (req, res) => {
const { url, fullPage = true, format = 'png' } = req.body;
const cacheKey = `${url}:${fullPage}:${format}`;
if (cache.has(cacheKey)) {
res.setHeader('Content-Type', `image/${format}`);
res.setHeader('X-Cache', 'HIT');
return res.send(cache.get(cacheKey));
}
const capture = await fetch('https://pagebolt.dev/api/v1/screenshot', {
method: 'POST',
headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ url, fullPage, blockBanners: true, format })
});
const image = Buffer.from(await capture.arrayBuffer());
cache.set(cacheKey, image);
res.setHeader('Content-Type', `image/${format}`);
res.setHeader('X-Cache', 'MISS');
res.send(image);
});
Extend to video
The same wrapper pattern works for narrated video recordings — expose a /video endpoint that your internal services can call without knowing the upstream API details:
app.post('/video', async (req, res) => {
const { steps, audioGuide, pace = 'slow' } = req.body;
const capture = await fetch('https://pagebolt.dev/api/v1/video', {
method: 'POST',
headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ steps, audioGuide, pace, frame: { enabled: true, style: 'macos' } })
});
const video = Buffer.from(await capture.arrayBuffer());
res.setHeader('Content-Type', 'video/mp4');
res.send(video);
});
Your CI pipeline, changelog automation, and onboarding video generator all call /video on your internal service — swap the upstream provider without touching any of them.
Add API key auth
If your internal API is exposed beyond localhost, add a simple key check:
app.use((req, res, next) => {
if (req.headers['x-internal-key'] !== process.env.INTERNAL_API_KEY) {
return res.status(401).json({ error: 'unauthorized' });
}
next();
});
Rotate INTERNAL_API_KEY independently of PAGEBOLT_API_KEY — your internal callers don't need to know the upstream key.
Get Started Free
100 requests/month, no credit card
Build your internal screenshot API on top of PageBolt — no browser to manage, no Puppeteer to maintain.
Get Your Free API Key →