Fetch only. No browser in your deploy.

Website screenshots
in Deno

Puppeteer ports for Deno need a local Chrome, and Deno Deploy isolates do not ship one. The standard library already has everything you need: fetch, URLSearchParams, and Deno.env.

200 renders a month, no card.

Why Chromium does not fit in Deno Deploy

On your laptop, Deno can drive Chrome through a Puppeteer port or Astral, provided a browser is installed and the script runs with --allow-run and --allow-net. On Deno Deploy that path closes: the isolate has no Chromium binary, no subprocess permission, and no writable disk to unpack one into. The usual advice is to connect to a remote browser over WebSocket, which brings back everything you were trying to avoid: session lifecycle, timeouts, and a second vendor billed by the minute.

A screenshot API keeps the whole integration inside the Deno standard runtime. The handler below is one fetch call and a Response. It runs identically with deno run locally and on Deno Deploy, and it needs no permissions beyond --allow-net and --allow-env.

References: Deno Deploy documentation.

The integration: one GET request

Call GET https://app.snap-render.com/v1/screenshot with the target in the url parameter and your key in the X-API-Key header. The response body is the finished image (or PDF when format=pdf). Ads and cookie banners are blocked by default.

Environment variable
# Local
export SNAPRENDER_API_KEY=sk_live_your_key_here
deno run --allow-net --allow-env main.ts

# Deno Deploy: Project > Settings > Environment Variables > SNAPRENDER_API_KEY
main.ts
const API_KEY = Deno.env.get('SNAPRENDER_API_KEY') ?? '';

Deno.serve(async (req: Request) => {
  const target = new URL(req.url).searchParams.get('url');
  if (!target) {
    return Response.json({ error: 'url query parameter is required' }, { status: 400 });
  }

  const params = new URLSearchParams({
    url: target,
    format: 'png',
    width: '1280',
    height: '800',
    full_page: 'true',
    dark_mode: 'false',
    cache: 'true',
    cache_ttl: '3600',
  });

  const upstream = await fetch(`https://app.snap-render.com/v1/screenshot?${params}`, {
    headers: { 'X-API-Key': API_KEY },
  });

  if (!upstream.ok) {
    const body = await upstream.json().catch(() => null);
    const code = body?.error?.code ?? 'UPSTREAM_ERROR';
    // 429 covers RATE_LIMITED (retry after a minute) and QUOTA_EXCEEDED (monthly)
    return Response.json({ error: code }, { status: upstream.status });
  }

  return new Response(upstream.body, {
    headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=3600' },
  });
});

Storing the image

On Deno Deploy there is no persistent disk, so stream the response straight to the client or forward the bytes to S3-compatible storage with the AWS SDK for JavaScript v3, which runs on Deno.

Save to disk from a script (local Deno)
// deno run --allow-net --allow-env --allow-write capture.ts https://example.com
const [target] = Deno.args;
const params = new URLSearchParams({ url: target, format: 'pdf', full_page: 'true' });

const res = await fetch(`https://app.snap-render.com/v1/screenshot?${params}`, {
  headers: { 'X-API-Key': Deno.env.get('SNAPRENDER_API_KEY') ?? '' },
});
if (!res.ok) {
  console.error(await res.text());
  Deno.exit(1);
}

await Deno.writeFile('page.pdf', new Uint8Array(await res.arrayBuffer()));
console.log('saved page.pdf, remaining credits:', res.headers.get('X-Remaining-Credits'));

Caching, quota, and errors

Caching

Caching is off by default. Pass cache=true and a cache_ttl in seconds (default 86400, maximum depends on plan). A repeat request for the same URL and parameters inside that window is served from the cache and does not count against your monthly quota. The X-Cache response header reports HIT or MISS, and X-Remaining-Credits shows what is left this month.

Error handling

  • 400 INVALID_URL or BLOCKED_URL: fix the input; private and local addresses are refused.
  • 401 missing or wrong X-API-Key.
  • 408 RENDER_TIMEOUT or TARGET_TIMEOUT: the page took longer than 30 seconds; retry with a simpler URL.
  • 429 RATE_LIMITED (per-minute burst, retry after a minute) or QUOTA_EXCEEDED (monthly allowance; the body names the next plan).
  • 502 RENDER_FAILED or TARGET_UNREACHABLE: the target site is down or refused the request.

Every error body is { "error": { "code", "message", "status" } }.

Full parameter list, POST with raw HTML, signed URLs, and batch jobs are in the API reference. The OpenAPI spec is at /openapi.json.

Deno Deploy screenshot FAQ

Can Deno Deploy run Puppeteer or Astral?+

Not with a local browser. Deploy isolates have no Chromium binary and no subprocess permission. Those libraries only work on Deploy when pointed at a remote browser endpoint over WebSocket.

Which permissions does the example need?+

Locally, --allow-net for the API call and --allow-env to read the key. The disk example adds --allow-write. On Deno Deploy permissions are implicit.

How do I get a PDF instead of an image?+

Pass format=pdf. The response is a PDF document of the rendered page. It is a page render, not a print-optimised layout, so long pages become tall PDF pages when full_page=true.

Do cached screenshots count against my quota?+

No. With cache=true and a cache_ttl, a repeat request for the same URL and parameters inside the window returns the stored image and is not billed as a render.

Is there a free tier?+

Yes. The free plan includes 200 renders a month with every feature enabled, and no card is required.

Other serverless runtimes

Language quickstarts: Node.js, Python, cURL. Cost comparison: Puppeteer on Lambda vs an API.

Or skip the setup entirely

Everything in this guide is one GET request with SnapRender. No browser to babysit, no timeouts to tune.

200 screenshots a month free. First render in under a minute.

Keep the browser out of your deploy

Get a free API key, 200 renders a month, no card.