Screenshot API · Node.js

Screenshot API for Node.js.

Shotium is a hosted screenshot API you call from Node.js with a single fetch: no Puppeteer, no Playwright, no headless Chrome in your deployment. Send a URL, get back PNG, JPEG or WebP bytes — with full-page capture, template-based OG images, and HMAC-signed URLs you can embed in public HTML.

How do I take a screenshot in Node.js?

One fetch call — built-in on Node 18+, no dependencies. Sign in, create an API key, and this runs as-is:

import { writeFile } from 'node:fs/promises'

const res = await fetch(
  'https://api.shotium.com/v1/screenshot?url=https://example.com&format=png',
  { headers: { Authorization: `Bearer ${process.env.SHOTIUM_KEY}` } },
)
if (!res.ok) throw new Error(`render failed: ${res.status}`)

await writeFile('shot.png', Buffer.from(await res.arrayBuffer()))

Full-page screenshots and viewport control

Set full_page to capture the entire scroll height (up to 20,000px). URLSearchParams keeps the query readable and stringifies every value for you:

const params = new URLSearchParams({
  url: 'https://news.ycombinator.com',
  full_page: 'true',
  format: 'webp',
  quality: '90',
  width: '1280',
})

const res = await fetch(`https://api.shotium.com/v1/screenshot?${params}`, {
  headers: { Authorization: `Bearer ${process.env.SHOTIUM_KEY}` },
})

Generate OG images from templates

POST typed parameters into one of five built-in templates and get a finished 1200×630 social card back — no HTML to write:

const res = await fetch('https://api.shotium.com/v1/og-image', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.SHOTIUM_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    template: 'blog',
    params: { title: 'Shipping fast without breaking things', author: 'Ada L.' },
    format: 'png',
  }),
})
if (!res.ok) throw new Error(`render failed: ${res.status}`)

How do I embed OG images without exposing my API key?

Sign the query string with your signing secret instead of sending your key. The signed URL is safe to print into public HTML — parameters are tamper-proof, and repeat fetches are served from the CDN edge for free:

import { createHmac } from 'node:crypto'

// RFC 3986: percent-encode the !'()* characters encodeURIComponent leaves bare
const rfc3986 = s => encodeURIComponent(s)
  .replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)

const params = {
  template: 'minimal',
  title: 'Less, but better.',
  uid: 'YOUR-UID',
}
const canonical = Object.entries(params)
  .sort(([a], [b]) => (a < b ? -1 : 1))
  .map(([k, v]) => `${rfc3986(k)}=${rfc3986(v)}`)
  .join('&')
const sig = createHmac('sha256', process.env.SHOTIUM_SIGNING_SECRET)
  .update(canonical).digest('hex')

const url = `https://api.shotium.com/v1/og-image?${canonical}&sig=${sig}`

Error handling

Every error is RFC 9457 problem+json with a stable type field you can branch on. Failed renders are never billed:

const res = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.SHOTIUM_KEY}` },
})
if (!res.ok) {
  const problem = await res.json() // { type, title, status, detail }
  if (problem.type === 'rate_limited') {
    const wait = Number(res.headers.get('Retry-After') ?? 1)
    // back off and retry
  } else if (problem.type === 'render_timeout') {
    // site took >30s — retry or skip
  }
}

Parameters at a glance

ParamDefaultNotes
urlrequiredhttp(s) URL to render
width / height1280 × 800Viewport, up to 3840 × 2160
full_pagefalseFull scroll height, ≤20,000px
formatpngpng | jpeg | webp
quality801–100, lossy formats only

Full reference — auth, OG templates, rate limits, error table — in the docs. Template parameters live on /og-templates.

Frequently asked questions

Do I need Puppeteer to take screenshots in Node.js?

No. Shotium runs the browsers server-side — from Node it's one fetch call, built in since Node 18. No Chromium download, no zombie processes, no monthly Puppeteer/Chrome version treadmill.

How do I take a full-page screenshot in Node.js?

Put full_page: 'true' in the URLSearchParams — note the string, since URLSearchParams stringifies everything anyway. The renderer scrolls, waits for lazy-loaded content to settle, and captures up to 20,000px. Long pages get large fast, so pair it with format=webp.

Does the API work in serverless and edge functions?

Yes — it's plain HTTPS fetch, so Vercel functions, AWS Lambda and Cloudflare Workers all work unchanged. For OG images, signed URLs go one further: the image renders on first crawler fetch, so your function never calls the API at all.

How should Node code react to a failed render?

Check res.ok before calling arrayBuffer() — fetch does not throw on HTTP errors, so an unchecked error response becomes a corrupt image file. On failure the body is RFC 9457 problem+json: rate_limited means back off for the Retry-After header, render_timeout means the target took over 30 seconds. Failed renders are never billed.

Try it on your own pages

Sign-up is GitHub OAuth and comes with 100 free render credits — no card. Failed renders never bill. Plans from $15/month on pricing.

Also available for: Python · PHP · Ruby · Go · Java · cURL