Sooner or later most apps need to turn a chunk of HTML and CSS into a flat image. Open Graph cards for link previews. Email and receipt images that have to look identical in every inbox. Certificates and tickets. Social share cards. Server-rendered charts. The markup already exists — you just need a picture of it.
Doing this on your own machine means running headless Chromium: install it, keep it patched, manage a process pool, handle the memory it eats, and wrap all of that in a queue so two requests don't fight over the same browser. An API does that part for you. You send markup, you get an image back.
The idea
You POST your markup in the html parameter to a single endpoint. It renders on real Chromium — the same engine your users run — and returns a URL to the finished image. Inline CSS, linked stylesheets, and web fonts all apply, so what you see in a browser is what you get in the file.
The endpoint is https://screenshotink.com/v1/capture. Authenticate with a bearer token (Authorization: Bearer sk_live_…). That's the whole contract.
cURL
The fastest way to confirm it works from your terminal:
curl "https://screenshotink.com/v1/capture" \
-H "Authorization: Bearer sk_live_…" \
--data-urlencode html="<h1 style='font-family:sans-serif'>Hello</h1>" \
-d format=png
The response is JSON:
{ "image_url": "https://screenshotink.com/…png" }
Fetch that URL to download the PNG, or hand it straight to an <img> tag.
Node.js
No SDK needed — fetch is built in on modern Node:
const res = await fetch("https://screenshotink.com/v1/capture", {
method: "POST",
headers: {
"Authorization": "Bearer sk_live_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
html: "<h1 style='font-family:sans-serif'>Hello from Node</h1>",
format: "png",
}),
});
const { image_url } = await res.json();
console.log(image_url);
image_url is the link to your rendered PNG. Store it, email it, or pipe it into whatever needs the picture.
Python
Using the requests library:
import requests
res = requests.post(
"https://screenshotink.com/v1/capture",
headers={"Authorization": "Bearer sk_live_…"},
json={
"html": "<h1 style='font-family:sans-serif'>Hello from Python</h1>",
"format": "png",
},
)
image_url = res.json()["image_url"]
print(image_url)
PHP
Plain cURL, no dependencies:
<?php
$ch = curl_init("https://screenshotink.com/v1/capture");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer sk_live_…",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"html" => "<h1 style='font-family:sans-serif'>Hello from PHP</h1>",
"format" => "png",
]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $response["image_url"];
If your host disables cURL, the same request works with file_get_contents and a stream context:
<?php
$context = stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer sk_live_…\r\n" .
"Content-Type: application/json\r\n",
"content" => json_encode([
"html" => "<h1 style='font-family:sans-serif'>Hello</h1>",
"format" => "png",
]),
],
]);
$response = json_decode(
file_get_contents("https://screenshotink.com/v1/capture", false, $context),
true
);
echo $response["image_url"];
Sizing and formats
A few parameters cover almost every layout you'll hit:
widthandheight— set exact pixel dimensions. For an Open Graph card you wantwidth=1200andheight=630; the render is clipped to that box. This is the right way to hit a fixed size every time instead of letting content decide.full_size— when content is taller than the viewport (a long receipt, a multi-row certificate), setfull_size=trueand the image grows to fit everything instead of cutting off atheight.format—png(default, sharp text and transparency),jpeg(smaller files for photo-heavy markup), orpdf(a vector-friendly document, good for invoices and printables).
Fonts behave the way they do in a browser. System font stacks render, and @font-face web fonts load over the network before the capture is taken, so your brand typeface shows up correctly. Both inline styles and linked stylesheets (<link rel="stylesheet">) are applied, so you can paste a real template in without flattening the CSS first.
One more thing worth knowing: identical renders are cached for 24 hours. If you ask for the same HTML and the same parameters again within that window, you get the cached image back and it doesn't count against your quota — handy when the same OG card gets requested by every crawler that visits a page.
ScreenshotInk runs on EU-hosted infrastructure, with 100 free renders a month to build and test against, and $9/mo for 2,000 when you go to production.
Try it: HTML to image tool. Building link previews specifically? The Open Graph image generator wires the 1200×630 sizing up for you.
Capture it with the API
Everything here runs on the ScreenshotInk API — 100 free captures a month, no card.