How to Use the Screenshot API with Node.js and JavaScript

2026-04-29 | Tags: [screenshot-api, nodejs, javascript, sdk, tutorials, integration]

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.