Skip to content

You can turn any public URL into a PNG or JPEG with one GET request. A real Chromium loads the page on our side, so you don't have to install or babysit a headless browser. Here's the call, what comes back, and the things a screenshot won't tell you.

Your first screenshot

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

That saves a 1280 x 800 PNG of GitHub's homepage, 76 KB when I ran it. The response body is the image itself. No JSON around it, no base64. -o writes it straight to a file.

-G with --data-urlencode makes curl build the query string and encode the target URL for you. It matters as soon as that URL has its own ? or & in it.

The key is free. Sign up, create one in the dashboard, and you get 75 requests a day.

Parameters

Parameter Default Allowed What it does
url required up to 2,048 characters The page to capture. https:// is added if you leave it off
width 1280 320 to 3840 Viewport width in pixels
height 800 200 to 2160 Viewport height in pixels. Ignored with full_page
full_page false true, false Capture the whole scrollable page
format png png, jpeg Image format
quality 80 1 to 100 JPEG quality. Does nothing for PNG

A phone-sized capture:

curl -G "https://apixies.io/api/v1/screenshot" \
     -H "X-API-Key: YOUR_API_KEY" \
     --data-urlencode "url=https://github.com" \
     -d width=375 -d height=812 \
     -o github-mobile.png

One thing to know about width. It sets the browser window, it doesn't scale the picture. At 640 pixels wide GitHub serves its mobile layout, hamburger menu and all. If you want a small image of the desktop page, capture at 1280 and shrink it yourself.

It's also only the window size. I pointed it at a page that echoes the User-Agent, and the browser introduces itself as HeadlessChrome on Linux, at normal pixel density. A site that picks its mobile version from the User-Agent will give you the desktop one in a narrow window. And that honest "Headless" is part of why some sites refuse to talk to it, more on that below.

PNG or JPEG

Same page, same 1280 x 800 viewport, three ways:

Request Size
format=png 76 KB
format=jpeg (quality 80) 57 KB
format=jpeg&quality=40 36 KB

PNG keeps text sharp, so use it when someone will read or compare the picture. JPEG is for thumbnails and anything you store by the thousand. On pages with photos and gradients the gap gets bigger: Laravel's homepage at 1200 x 630 was 200 KB as PNG and 92 KB as JPEG.

What comes back

On success you get 200, a Content-Type of image/png or image/jpeg, and the bytes. Errors are JSON in the usual envelope, so check the content type before you write a file.

Status Code When
401 MISSING_AUTH No X-API-Key header
422 VALIDATION_FAILED A parameter is out of range, like width=100 or format=webp
422 HOST_NOT_RESOLVED The host name doesn't exist
400 RESTRICTED_TARGET The URL points at a private or local address
502 SCREENSHOT_FAILED The browser couldn't load the page
429 DAILY_QUOTA_EXCEEDED You've used today's requests

Private addresses are refused on purpose. Ask for http://192.168.1.1 and you get this, in about a tenth of a second:

{
  "status": "error",
  "http_code": 400,
  "code": "RESTRICTED_TARGET",
  "message": "The target URL points to a restricted address."
}

So it can't see your staging server unless that server is reachable from the internet.

SCREENSHOT_FAILED covers everything that stops Chromium from loading the page. I got it for https://expired.badssl.com, because the browser won't go past an expired certificate.

What a 200 doesn't mean

A 200 means the browser loaded something and took a picture of it. It says nothing about what's in the picture.

  • Error pages are pages. https://github.com/this-page-does-not-exist-zz9 returns 200 and a nice PNG of GitHub's 404 page. If you need the real status, ask the Link Checker first.
  • Bot walls. Stack Overflow gave me a Cloudflare "Performing security verification" screen. G2 gave me "Access is temporarily restricted". Both arrived as perfectly valid images.
  • Consent dialogs. The Guardian and Der Spiegel both came back with a cookie dialog covering the middle of the page. There's no parameter to click it away.

So look at your results, at least while you're setting things up. The link preview guide has a cheap check that catches the bot walls.

How long it takes

The browser waits until the page's network traffic goes quiet, so the time is mostly the target site's. From my machine: example.com took 1.8 seconds, GitHub about 2.5, Laravel's homepage a bit over 4. Give your HTTP client a timeout of a minute and don't put this call in the path of a page load.

In code

All three save the image and throw on a JSON error.

JavaScript

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

async function screenshot(url, file, options = {}) {
  const params = new URLSearchParams({ url, ...options });
  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(`${body.code}: ${body.message}`);
  }

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

await screenshot("https://github.com", "github.jpg", { format: "jpeg", width: "1280" });

Python

import os
import requests

def screenshot(url, file, **options):
    res = requests.get(
        "https://apixies.io/api/v1/screenshot",
        params={"url": url, **options},
        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"{body['code']}: {body['message']}")

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

screenshot("https://github.com", "github.jpg", format="jpeg", width=1280)

PHP

function screenshot(string $url, string $file, array $options = []): void
{
    $ch = curl_init('https://apixies.io/api/v1/screenshot?' . http_build_query(['url' => $url] + $options));
    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(($error['code'] ?? 'ERROR') . ': ' . ($error['message'] ?? 'no response'));
    }

    file_put_contents($file, $body);
}

screenshot('https://github.com', 'github.jpg', ['format' => 'jpeg', 'width' => 1280]);

You can try the same thing without code in the Screenshot tool.

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