How to Use the Screenshot API with Node.js and JavaScript
Node.js is the second most common runtime for screenshot API integrations — backend services, Express apps, Next.js, and serverless functions all reach for JavaScript. This guide covers the full integration from a single call through production-ready patterns.
Installation
No SDK required — native fetch (Node 18+) or axios works:
# Optional: axios for older Node versions or convenience
npm install axios
Basic Usage (Native Fetch, Node 18+)
async function screenshot(url, apiKey) {
const params = new URLSearchParams({ url, key: apiKey });
const resp = await fetch(
`https://hermesforge.dev/api/screenshot?${params}`
);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return Buffer.from(await resp.arrayBuffer());
}
// Save to file
import { writeFileSync } from 'fs';
const image = await screenshot('https://example.com', 'YOUR_API_KEY');
writeFileSync('screenshot.png', image);
All Parameters
async function screenshotFull({
url,
apiKey,
width = 1280,
height = 800,
format = 'png', // 'png' or 'webp'
fullPage = false,
waitFor = 'networkidle' // 'load', 'domcontentloaded', 'networkidle'
}) {
const params = new URLSearchParams({
url,
key: apiKey,
width: String(width),
height: String(height),
format,
full_page: String(fullPage),
wait_for: waitFor
});
const resp = await fetch(
`https://hermesforge.dev/api/screenshot?${params}`
);
if (!resp.ok) throw new Error(`Screenshot failed: HTTP ${resp.status}`);
return Buffer.from(await resp.arrayBuffer());
}
A Reusable Client Class
import { createHash } from 'crypto';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
class ScreenshotClient {
constructor(apiKey, { cacheDir = null } = {}) {
this.apiKey = apiKey;
this.cacheDir = cacheDir;
if (cacheDir) mkdirSync(cacheDir, { recursive: true });
}
_cacheKey(url, params) {
const data = url + JSON.stringify(Object.entries(params).sort());
return createHash('md5').update(data).digest('hex');
}
async capture(url, {
width = 1280,
format = 'png',
fullPage = false,
waitFor = 'networkidle',
useCache = true
} = {}) {
const params = { width, format, full_page: fullPage, wait_for: waitFor };
if (this.cacheDir && useCache) {
const cacheFile = join(this.cacheDir, `${this._cacheKey(url, params)}.${format}`);
if (existsSync(cacheFile)) return readFileSync(cacheFile);
}
const qs = new URLSearchParams({
url, key: this.apiKey,
width: String(width), format,
full_page: String(fullPage),
wait_for: waitFor
});
const resp = await fetch(`https://hermesforge.dev/api/screenshot?${qs}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = Buffer.from(await resp.arrayBuffer());
if (this.cacheDir && useCache) {
const cacheFile = join(this.cacheDir, `${this._cacheKey(url, params)}.${format}`);
writeFileSync(cacheFile, data);
}
return data;
}
async captureToFile(url, outputPath, options = {}) {
const data = await this.capture(url, options);
writeFileSync(outputPath, data);
return outputPath;
}
}
// Usage
const client = new ScreenshotClient('YOUR_API_KEY', { cacheDir: '/tmp/screenshots' });
const data = await client.capture('https://example.com', { width: 1440, fullPage: true });
await client.captureToFile('https://example.com', 'output.png');
Error Handling with Retry
async function screenshotWithRetry(url, apiKey, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const params = new URLSearchParams({ url, key: apiKey });
const resp = await fetch(`https://hermesforge.dev/api/screenshot?${params}`);
if (resp.ok) return Buffer.from(await resp.arrayBuffer());
if (resp.status === 429) {
const wait = 60_000 * (attempt + 1);
console.log(`Rate limited — waiting ${wait / 1000}s (attempt ${attempt + 1}/${maxRetries})`);
await new Promise(r => setTimeout(r, wait));
continue;
}
if (resp.status === 422) throw new Error(`Invalid params: ${await resp.text()}`);
throw new Error(`HTTP ${resp.status}`);
}
throw new Error(`Failed after ${maxRetries} attempts`);
}
Concurrent Batch Processing
async function batchScreenshot(urls, apiKey, { concurrency = 5 } = {}) {
const results = [];
// Process in chunks to respect concurrency limit
for (let i = 0; i < urls.length; i += concurrency) {
const chunk = urls.slice(i, i + concurrency);
const chunkResults = await Promise.allSettled(
chunk.map(async url => {
const params = new URLSearchParams({ url, key: apiKey });
const resp = await fetch(`https://hermesforge.dev/api/screenshot?${params}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return { url, data: Buffer.from(await resp.arrayBuffer()) };
})
);
results.push(...chunkResults);
}
return results.map((r, i) =>
r.status === 'fulfilled'
? { url: urls[i], data: r.value.data, status: 'ok' }
: { url: urls[i], error: r.reason.message, status: 'failed' }
);
}
// Usage
const urls = ['https://example.com', 'https://nodejs.org', 'https://npmjs.com'];
const results = await batchScreenshot(urls, 'YOUR_API_KEY', { concurrency: 3 });
for (const r of results) {
if (r.status === 'ok') {
writeFileSync(`${new URL(r.url).hostname}.png`, r.data);
console.log(`Saved: ${r.url}`);
} else {
console.error(`Failed: ${r.url} — ${r.error}`);
}
}
Express Middleware
import express from 'express';
const app = express();
const API_KEY = process.env.SCREENSHOT_API_KEY;
app.get('/screenshot', async (req, res) => {
const { url } = req.query;
if (!url) return res.status(400).json({ error: 'Missing url parameter' });
try {
const params = new URLSearchParams({
url, key: API_KEY,
width: '1280', format: 'webp', wait_for: 'networkidle'
});
const upstream = await fetch(`https://hermesforge.dev/api/screenshot?${params}`);
if (!upstream.ok) return res.status(502).json({ error: `Upstream HTTP ${upstream.status}` });
res.set('Content-Type', 'image/webp');
res.set('Cache-Control', 'public, max-age=3600');
res.send(Buffer.from(await upstream.arrayBuffer()));
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(3000);
Next.js API Route
// pages/api/screenshot.js (Next.js Pages Router)
export default async function handler(req, res) {
const { url } = req.query;
if (!url) return res.status(400).json({ error: 'Missing url' });
const params = new URLSearchParams({
url, key: process.env.SCREENSHOT_API_KEY,
width: '1280', format: 'png', wait_for: 'networkidle'
});
const upstream = await fetch(`https://hermesforge.dev/api/screenshot?${params}`);
if (!upstream.ok) return res.status(502).end();
const data = await upstream.arrayBuffer();
res.setHeader('Content-Type', 'image/png');
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate');
res.send(Buffer.from(data));
}
// app/api/screenshot/route.js (Next.js App Router)
export async function GET(request) {
const { searchParams } = new URL(request.url);
const url = searchParams.get('url');
if (!url) return new Response('Missing url', { status: 400 });
const params = new URLSearchParams({
url, key: process.env.SCREENSHOT_API_KEY,
width: '1280', format: 'png'
});
const upstream = await fetch(`https://hermesforge.dev/api/screenshot?${params}`);
if (!upstream.ok) return new Response(null, { status: 502 });
return new Response(upstream.body, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=3600'
}
});
}
Check Quota
async function checkQuota(apiKey) {
const resp = await fetch(
`https://hermesforge.dev/api/usage?key=${apiKey}`
);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
const quota = await checkQuota('YOUR_API_KEY');
console.log(`Used today: ${quota.calls_this_period}`);
console.log(`Remaining: ${quota.rate_limit_remaining_today}`);
Rate Limits
| Tier | Daily limit | Cost |
|---|---|---|
| Free | 50/day | $0 |
| Starter | 200/day | $4/30 days |
| Pro | 1,000/day | $9/30 days |
| Business | 5,000/day | $29/30 days |
Get your free API key — 50/day, no sign-up required. Upgrade when you need more.