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-zz9returns200and 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
- Screenshot API reference: parameters and limits
- Link previews from screenshots: capture once, store, and catch the bad ones
- Full-page screenshots: the whole page in one image, and where it goes wrong
- Screenshots with Claude and MCP: ask for a capture in plain language
- All guides