Skip to content
tutorial

Capture website screenshots with one API call

| 6 min read
Multiple browser screenshots arranged in a grid
Photo by Hal Gatewood on Unsplash

You need a screenshot of a webpage. Maybe you're building link previews for a chat app. Maybe you're running visual regression tests in CI. Maybe you're generating PDF reports that embed a live dashboard snapshot. The obvious answer is Puppeteer or Playwright on your server. The obvious problem: a headless Chromium binary eats 200-500 MB of memory, takes 3-8 seconds to cold start, needs OS-level dependencies, and turns your Docker image into a 1 GB artifact. All that to capture website screenshots programmatically.

There's a simpler path. Send the URL to a screenshot API, get back an image. No browser binary on your server, no memory spikes, no Chromium version mismatches across environments.

Capture a website screenshot with one POST request

Send a URL to the /v1/screenshot/capture endpoint and get back a screenshot as PNG, JPEG, or WebP. The API runs a full Chromium instance on Cloudflare's edge network, so all JavaScript, CSS, and web fonts render correctly.

curl -X POST https://api.botoi.com/v1/screenshot/capture \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://github.com",
    "width": 1280,
    "height": 800,
    "format": "png"
  }'

Response:

{
  "success": true,
  "data": {
    "url": "https://api.botoi.com/screenshots/a1b2c3d4.png",
    "format": "png",
    "width": 1280,
    "height": 800,
    "fullPage": false,
    "size_bytes": 184320
  }
}

The response gives you a direct URL to the captured image along with the format, dimensions, and file size. You can download, cache, or serve that URL directly in your application. No base64 decoding or file handling required.

Multiple browser windows showing different website screenshots
Photo by Hal Gatewood on Unsplash

Control viewport, format, and timing

The default screenshot captures a 1280x800 viewport in PNG format. That covers most use cases, but you'll often need more control. Here are the parameters you can set:

  • width / height: Viewport dimensions in pixels. Use 1440x900 for desktop, 390x844 for iPhone 14, or 768x1024 for iPad.
  • format: Output as png (lossless, larger), jpeg (lossy, smaller), or webp (best compression for web use).
  • fullPage: Set to true to capture the entire scrollable page, not only what fits in the viewport. The API scrolls through the page and stitches the result into one tall image.
  • delay: Milliseconds to wait after page load before capturing. Set this to 2000-3000 for SPAs that fetch data on mount and render client-side. Without it, you'll screenshot a loading spinner.

Here's a request that captures a full-page WebP screenshot of a JavaScript-rendered SPA with a 3-second delay:

curl -X POST https://api.botoi.com/v1/screenshot/capture \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-spa.vercel.app",
    "width": 1440,
    "height": 900,
    "format": "webp",
    "fullPage": true,
    "delay": 3000
  }'

Generate link previews for your app

Link previews are one of the most common reasons to capture webpage screenshots programmatically. When a user pastes a URL in your chat app, CMS, or project management tool, you want to show a thumbnail. Open Graph images cover some links, but many pages don't set them, and the ones that do often use generic brand images instead of actual page content.

This Node.js server exposes a /preview endpoint that captures and caches thumbnails at 1200x630 (the standard Open Graph image size):

import express from "express";

const app = express();
app.use(express.json());

const screenshotCache = new Map();

async function captureScreenshot(url) {
  // Return cached version if available
  if (screenshotCache.has(url)) {
    return screenshotCache.get(url);
  }

  const res = await fetch("https://api.botoi.com/v1/screenshot/capture", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      url,
      width: 1200,
      height: 630,
      format: "webp",
    }),
  });

  const { data } = await res.json();
  screenshotCache.set(url, data.url);
  return data.url;
}

app.get("/preview", async (req, res) => {
  const { url } = req.query;

  if (!url) {
    return res.status(400).json({ error: "url parameter required" });
  }

  const thumbnailUrl = await captureScreenshot(url);
  res.json({
    original_url: url,
    thumbnail: thumbnailUrl,
    dimensions: "1200x630",
  });
});

app.listen(3000);

Call /preview?url=https://stripe.com/docs and you get back a WebP thumbnail URL showing the actual page content. The in-memory cache prevents duplicate captures for the same URL. In production, swap that Map for Redis or a CDN cache with a TTL of 24-48 hours so previews stay fresh.

Visual regression testing in CI

Visual regression testing catches layout breakage that unit tests miss. A CSS change that passes all tests can still push your pricing page headline off-screen. The traditional approach needs Playwright and a headless browser running in your CI runner. That adds 2-3 minutes to your pipeline and uses 500+ MB of disk.

This GitHub Actions workflow captures screenshots of key pages from both production and your PR preview, then outputs a comparison table in the PR summary:

name: Visual regression check

on:
  pull_request:
    branches: [main]

jobs:
  screenshot-diff:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Capture baseline screenshots
        run: |
          PAGES=("/" "/pricing" "/docs" "/blog")
          BASE_URL="https://your-app.com"
          mkdir -p screenshots/baseline

          for PAGE in "${PAGES[@]}"; do
            FILENAME=$(echo "$PAGE" | tr '/' '-' | sed 's/^-//')
            [ -z "$FILENAME" ] && FILENAME="home"

            curl -s -X POST https://api.botoi.com/v1/screenshot/capture \
              -H "Content-Type: application/json" \
              -H "Authorization: Bearer ${{ secrets.BOTOI_API_KEY }}" \
              -d "{
                \"url\": \"$BASE_URL$PAGE\",
                \"width\": 1280,
                \"height\": 800,
                \"format\": \"png\",
                \"fullPage\": true
              }" | jq -r '.data.url' > "screenshots/baseline/$FILENAME.url"

            echo "Captured baseline: $PAGE"
          done

      - name: Capture PR preview screenshots
        run: |
          PAGES=("/" "/pricing" "/docs" "/blog")
          PREVIEW_URL="${{ github.event.pull_request.head.repo.html_url }}"
          mkdir -p screenshots/preview

          for PAGE in "${PAGES[@]}"; do
            FILENAME=$(echo "$PAGE" | tr '/' '-' | sed 's/^-//')
            [ -z "$FILENAME" ] && FILENAME="home"

            curl -s -X POST https://api.botoi.com/v1/screenshot/capture \
              -H "Content-Type: application/json" \
              -H "Authorization: Bearer ${{ secrets.BOTOI_API_KEY }}" \
              -d "{
                \"url\": \"$PREVIEW_URL$PAGE\",
                \"width\": 1280,
                \"height\": 800,
                \"format\": \"png\",
                \"fullPage\": true
              }" | jq -r '.data.url' > "screenshots/preview/$FILENAME.url"

            echo "Captured preview: $PAGE"
          done

      - name: Compare screenshots
        run: |
          echo "## Visual regression report" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          for BASELINE in screenshots/baseline/*.url; do
            NAME=$(basename "$BASELINE" .url)
            BASELINE_URL=$(cat "$BASELINE")
            PREVIEW_URL=$(cat "screenshots/preview/$NAME.url")
            echo "| $NAME | [baseline]($BASELINE_URL) | [preview]($PREVIEW_URL) |" >> $GITHUB_STEP_SUMMARY
          done

The workflow captures four pages from your production site and the PR preview URL. Screenshot URLs are logged to the GitHub step summary as a comparison table so reviewers can visually check each page. No browser binaries in CI, no Playwright setup, no Docker layer caching.

For automated pixel-level diffing, pipe the screenshot URLs into a comparison tool like pixelmatch or resemblejs and fail the workflow if the diff exceeds a threshold.

Comparison: screenshot API vs self-hosted Puppeteer

Running your own headless browser gives you full control, but that control comes with operational cost. Here's how the two approaches stack up:

Feature                  | Screenshot API             | Self-hosted Puppeteer
─────────────────────────|────────────────────────────|──────────────────────────
Setup time               | 0 min (one HTTP call)      | 30-60 min (Docker, deps)
Browser binary           | Managed by API             | You maintain Chromium
Memory usage             | 0 MB on your server        | 200-500 MB per instance
Cold start               | None (edge network)        | 3-8 sec (browser launch)
Scaling                  | Handled automatically      | Manual (container pool)
Maintenance              | None                       | OS patches, Chrome updates
Cost (low volume)        | Free (5 req/min)           | Server cost + your time
Cost (high volume)       | API plan (~$20/mo)         | $50-200/mo (server + ops)
Full-page capture        | Built-in parameter         | Custom scroll logic
Format options           | PNG, JPEG, WebP            | Depends on your code
JavaScript rendering     | Built-in delay param       | Custom waitForSelector

The API wins on setup speed and maintenance. Self-hosted Puppeteer wins when you need fine-grained browser control (intercepting network requests, injecting cookies for authenticated pages, or running custom JavaScript before capture). For most screenshot use cases; link previews, visual testing, report thumbnails, social cards; the API approach covers what you need without the infrastructure overhead.

Key points

- One POST request captures any public URL as PNG, JPEG, or WebP
- Viewport width, height, full-page mode, and JS delay are all configurable
- No Puppeteer, Chromium, or headless browser setup on your side
- Free tier: 5 screenshots per minute, no API key needed
- Use cases: link previews, visual regression testing, PDF reports, social cards
- The API runs Chromium on the edge, so JS-heavy SPAs render correctly

The free tier at 5 requests per minute works for development, prototyping, and low-traffic link previews. For CI pipelines and production apps that need higher throughput, add your API key in the Authorization: Bearer header. Check the API docs for the full parameter reference and response schema.

Frequently asked questions

How do I take screenshots of websites programmatically?
Send a POST request to the botoi screenshot API at /v1/screenshot/capture with the target URL, desired viewport size, and output format. The API returns a base64-encoded image or a direct URL to the captured screenshot. No browser binary or Puppeteer setup required.
What is the best screenshot API for developers?
The best screenshot API depends on your needs. For quick integration with zero infrastructure, the botoi screenshot API handles viewport control, full-page capture, format selection (PNG, JPEG, WebP), and JavaScript rendering delay. Anonymous access is free at 5 requests per minute.
How do I capture a full page screenshot of a website?
Set the "fullPage" parameter to true in your API request. The API scrolls through the entire page and stitches the result into a single image. This captures content below the fold that a standard viewport screenshot would miss.
Can I capture screenshots of JavaScript-rendered pages?
Yes. Use the "delay" parameter to give the page time to execute JavaScript before the screenshot is taken. A delay of 2000-3000 milliseconds works for most SPAs built with React, Vue, or Angular. The API runs a full Chromium browser, so all client-side rendering completes normally.
Is the screenshot API free to use?
Anonymous access is available at 5 requests per minute with IP-based rate limiting. No API key or account required. Paid plans remove the rate limit and support higher concurrency for production workloads like link preview generation or visual regression testing.

Try this API

URL Metadata API — interactive playground and code examples

More tutorial posts

Start building with botoi

150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.