Skip to content

Add full_page=true to a Screenshot API call and you get the whole page in one image, top of the header to bottom of the footer. It's what you want for archiving a page, reviewing a layout, or diffing a release against the last one. It's also where modern pages misbehave the most, so half of this guide is about that.

The call

curl -G "https://apixies.io/api/v1/screenshot" \
     -H "X-API-Key: YOUR_API_KEY" \
     --data-urlencode "url=https://laravel.com" \
     -d full_page=true \
     -o laravel-full.png

I got a PNG of 1280 x 6282 pixels. width still decides how the page lays itself out. height is ignored: I sent height=300 with the same request and got the same 6282 pixels back.

Tall images get heavy, and this is where JPEG earns its keep:

Request Dimensions Size
full_page=true 1280 x 6282 1.96 MB
full_page=true&format=jpeg 1280 x 6282 668 KB
full_page=true&format=jpeg&width=375 375 x 9742 406 KB

The last row is the phone-width version. It's narrower and half as long again, because everything that sat side by side on desktop now stacks. That's the one to look at when you're checking a responsive layout.

Each of those took four to five seconds from my machine. GitHub's homepage, 11,198 pixels tall, took about three.

Where it goes wrong

A full-page capture doesn't scroll. The browser loads the page, waits for the network to go quiet, measures the document and photographs all of it in one go. Anything the page only does when a person scrolls never happens.

Things that appear on scroll stay empty. GitHub's homepage is the clearest case I found. The headings and text are all there, all 11,198 pixels of them, but several of the big feature panels are plain black boxes. Whatever fills them waits for the panel to scroll into view, and nothing ever scrolled. Laravel's homepage came out complete, pictures and all.

You can't tell which kind a site is without looking. So look, once, before you build on it.

Infinite feeds stop at the first batch. dev.to's front page came back 5,393 pixels tall and ends, fittingly, with the word "loading..." under the last post. You get the first page of the feed and never the second.

Consent dialogs and bot walls show up here exactly as they do in a normal capture. The main screenshot guide has examples, and the link preview guide has a way to catch the bot walls.

If it's your own site that captures badly, look at how the missing parts get loaded. Laravel's page marks 10 of its 11 images with loading="lazy" and still photographed fine, so the browser's own lazy loading seems to cope. Content that a script adds when it scrolls into view is what goes missing.

Visual checks after a deploy

The classic use is comparing today's pages with yesterday's. Capture a handful of key pages after each deploy, diff them against the stored baseline with something like pixelmatch or ImageMagick's compare, and have a person look when the difference is large.

Two limits shape how you do it.

The target has to be reachable from the internet. localhost, 10.x and the other private ranges are refused with RESTRICTED_TARGET, so this works against a public staging or production URL and not against the CI runner itself.

And the free tier is 75 requests a day. Ten pages at two widths after every deploy is 20 requests, so you've got room for three deploys a day. Pick the pages that matter.

Use PNG for diffing. JPEG artifacts shift a little between runs and show up as noise in the diff.

Python, a batch run over a few pages:

import os
import requests

PAGES = {
    "home": "https://laravel.com",
    "docs": "https://laravel.com/docs",
}

def capture_full_page(url, file, width=1280):
    res = requests.get(
        "https://apixies.io/api/v1/screenshot",
        params={"url": url, "full_page": "true", "width": width},
        headers={"X-API-Key": os.environ["APIXIES_API_KEY"]},
        timeout=90,
    )
    if not res.headers.get("Content-Type", "").startswith("image/"):
        body = res.json()
        raise RuntimeError(f"{url}: {body['code']}: {body['message']}")

    with open(file, "wb") as f:
        f.write(res.content)

for name, url in PAGES.items():
    for width in (1280, 375):
        capture_full_page(url, f"{name}-{width}.png", width)
        print(f"saved {name}-{width}.png")

JavaScript

import { writeFile } from "node:fs/promises";

async function captureFullPage(url, file, width = 1280) {
  const params = new URLSearchParams({ url, full_page: "true", width: String(width) });
  const res = await fetch(`https://apixies.io/api/v1/screenshot?${params}`, {
    headers: { "X-API-Key": process.env.APIXIES_API_KEY },
  });

  if (!res.headers.get("content-type")?.startsWith("image/")) {
    const body = await res.json();
    throw new Error(`${url}: ${body.code}: ${body.message}`);
  }

  await writeFile(file, Buffer.from(await res.arrayBuffer()));
}

await captureFullPage("https://laravel.com", "home-1280.png");
await captureFullPage("https://laravel.com", "home-375.png", 375);

PHP

function captureFullPage(string $url, string $file, int $width = 1280): void
{
    $query = http_build_query(['url' => $url, 'full_page' => 'true', 'width' => $width]);
    $ch = curl_init("https://apixies.io/api/v1/screenshot?$query");
    curl_setopt_array($ch, [
        CURLOPT_HTTPHEADER => ['X-API-Key: ' . getenv('APIXIES_API_KEY')],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 90,
    ]);
    $body = curl_exec($ch);
    $type = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
    curl_close($ch);

    if (! str_starts_with($type, 'image/')) {
        $error = json_decode((string) $body, true);
        throw new RuntimeException("$url: " . ($error['code'] ?? 'ERROR') . ': ' . ($error['message'] ?? 'no response'));
    }

    file_put_contents($file, $body);
}

captureFullPage('https://laravel.com', 'home-1280.png');

A cheap sanity check for any of them: read the image height after saving. A page that's suddenly a third as tall as last week has lost something, and you'll know before you open the file.

Keeping a record of a page

The other common use is proof of what a page said on a given day: a price list, terms, a job ad. A full-page capture is good for that because it shows the page the way a visitor saw it. Store the file with the URL and the capture time, and keep the PNG, since it's the one you may need to read closely later.

It's a picture, though, not an archive. Text isn't selectable, links are gone, and anything behind a click or a login isn't in it. If you need the text as well, save the page's HTML next to the image. The HTML to Markdown API turns it into something readable.

Full page or viewport

Use the viewport (the default) for thumbnails, cards and link previews. You want a fixed size, and the top of the page is the recognisable part.

Use full_page when a person or a diff tool is going to inspect the page itself.

Next steps

Try the URL Screenshot Generator API

Free tier is for development & small projects. 75 requests/day with a registered account.

cookies

We use analytics cookies to see how the site gets used. Nothing loads until you accept. Privacy policy