How to Screenshot a Website from a URL (No Browser Needed)

javascript dev.to

Every approach to capturing a screenshot from a URL falls into one of three categories: call an API, run a local headless browser, or use a client-side library. Each has a different setup cost, maintenance burden, and failure mode. Here is how to pick the right one and use it correctly.

The three approaches

Screenshot API. You send a POST request with the target URL and receive a hosted image URL in the response. The headless browser runs in the cloud. No binaries to install, no drivers to version-match, no infrastructure to keep alive.

Puppeteer or Playwright. You launch headless Chrome locally, navigate to the URL, and call .screenshot(). Full control, but you own the Chromium binary, the Node.js process, and every failure mode that comes with them.

html2canvas. Runs in the browser and serializes what is already in the DOM to a canvas. Works only client-side and misses content loaded by JavaScript after the initial render.

For server-side, automated, or high-volume capture, the API path is almost always the right call. For local dev scripts where you already have Node.js set up, Puppeteer and Playwright work well. html2canvas is the only viable option when you need a screenshot of what a user is looking at inside a browser tab.

Using a screenshot API

One POST request captures any public URL:

curl https://api.grabbit.live/v1/grabs \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "width": 1280,
    "height": 720,
    "format": "webp"
  }'
Enter fullscreen mode Exit fullscreen mode

The response includes a hosted image_url you can store and serve directly:

{"id":"grb_01jx...","status":"done","target_url":"https://example.com","image_url":"https://cdn.grabbit.live/grabs/grb_01jx....webp","width":1280,"height":720,"format":"webp","bytes":62140,"execution_ms":940,"created_at":"2026-06-15T09:00:00.000Z"}
Enter fullscreen mode Exit fullscreen mode

Width must be between 320 and 1920 pixels; height between 240 and 1080. Format options are png, jpeg, and webp (webp is the best default for file size). Set "full_page": true to capture the full scrollable height instead of the viewport. Add "delay_ms": 1000 if the page loads content client-side before it is visible.

The same call in Python, no browser required:

import requests

resp = requests.post(
    "https://api.grabbit.live/v1/grabs",
    headers={"Authorization": "Bearer sk_live_..."},
    json={
        "url": "https://example.com",
        "width": 1280,
        "height": 720,
        "format": "webp",
    },
)
data = resp.json()
print(data["image_url"])
Enter fullscreen mode Exit fullscreen mode

No chromedriver, no binary downloads, no async process to tear down after the call.

Using Puppeteer

Puppeteer gives you direct control of Chromium at the cost of managing the binary yourself:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 720 });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'screenshot.png' });
await browser.close();
Enter fullscreen mode Exit fullscreen mode

Puppeteer ships Chromium at around 300 MB. It works well for local scripts, but deploying it to serverless runtimes or CI agents that lack the right system libraries is a recurring headache. See full-page screenshots with Puppeteer for the fullPage: true option and the tradeoffs versus an API.

Using Playwright

Playwright's API is nearly identical and adds Firefox and WebKit support:

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();
await page.setViewportSize({ width: 1280, height: 720 });
await page.goto('https://example.com');
await page.screenshot({ path: 'screenshot.png', fullPage: true });
await browser.close();
Enter fullscreen mode Exit fullscreen mode

Playwright's waitForSelector and waitForLoadState options give it a slight edge for pages with delayed content. The infrastructure burden is the same as Puppeteer.

When to use which

Scenario Best choice
Production automation or CI Screenshot API
Serverless function Screenshot API
Bulk capture of many URLs Screenshot API with async mode
Local dev script or one-off audit Puppeteer or Playwright
Capture existing browser content (client-side) html2canvas

Adding a headless browser process to a server that already handles your application workload is a maintenance burden. The API separates the capture concern entirely and keeps your deployment small.

Capturing multiple URLs

For bulk jobs, pass Prefer: respond-async to queue the capture without waiting for it to finish:

import requests

urls = [
    "https://example.com",
    "https://example.com/about",
    "https://example.com/pricing",
]

for url in urls:
    resp = requests.post(
        "https://api.grabbit.live/v1/grabs",
        headers={
            "Authorization": "Bearer sk_live_...",
            "Prefer": "respond-async",
        },
        json={"url": url, "width": 1280, "height": 720, "format": "webp"},
    )
    job = resp.json()
    print(job["id"], job["status"])  # poll GET /api/v1/grabs/{id} until "done"
Enter fullscreen mode Exit fullscreen mode

Prefer: respond-async returns 202 immediately with a job ID. Poll GET /api/v1/grabs/{id} until status is "done", then read image_url. You can queue up to 10 concurrent jobs per team.

Waiting for dynamic content

If the target page loads content after the initial HTML is served, a plain page.goto or an instant API call may fire before the content is visible. Two options:

  • "delay_ms": 1500 waits a fixed number of milliseconds after the page loads before capturing. Simple, but guesswork.
  • "selector": "#main-content" waits until that element is present in the DOM. Tied to a real content signal, not an arbitrary timer.

For pure server-rendered pages with no client-side hydration, neither is needed.

Next steps

The automated screenshots guide covers scheduling, webhooks, and the async job queue for production capture pipelines. For capturing pages that extend beyond the viewport, see the full-page screenshot guide.


Originally published on the Grabbit blog.

Source: dev.to

arrow_back Back to Tutorials