HTML to Image API — Render HTML to PNG, WebP, or JPEG
Every SaaS product eventually needs to generate images from dynamic data. OG cards for social sharing, PDF invoices, course certificates, personalized email headers. The content changes per user or per page, so static assets don't work. You need a template engine that outputs pixels.
Puppeteer or Playwright is the obvious path: spin up headless Chrome, load your HTML, call page.screenshot(). It works on your laptop. In production, it means managing Chromium processes, handling memory leaks, installing fonts on every server, and dealing with a 300 MB binary that doesn't fit in AWS Lambda's deployment limit.
With the html parameter, you send any HTML string and get an image back. A full Chromium instance renders it on our end. No browser to manage, no CSS restrictions, no deployment headaches.
Rendering HTML to an image with one API call
Pass your HTML via the html parameter instead of a url:
curl -X POST https://screenshotrun.com/api/v1/screenshots \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "Hello World
Rendered in real Chromium.
",
"width": 600,
"height": 400,
"format": "png"
}'
The API queues a render job, loads your HTML in Chromium, waits for network activity to settle, and captures the viewport. The response includes a download URL for the generated image. The HTML renders exactly as it would in a Chrome browser tab, because that's literally what it is.
You can use any image format the API supports: PNG for lossless output, WebP for smaller file sizes, JPEG for photographs, or even PDF if you need a paginated document from the same template.
Full Chromium rendering, no CSS restrictions
This is where the API differs from tools like Satori and Vercel OG. Those tools convert a JSX component to SVG, not a real browser render. Satori supports a subset of CSS: flexbox works, but display: grid doesn't. position: absolute works, but calc() behaves unpredictably. Custom fonts must be registered manually and fit within a 500 KB total bundle limit.
The screenshotrun API has none of these restrictions. It runs a real Chromium instance, so everything Chrome supports is available:
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 20px; padding: 40px; font-family: 'Inter', sans-serif;">
<div style="background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 12px; padding: 30px; color: white;">
<h2 style="margin: 0;">Monthly Report</h2>
<p style="opacity: 0.8;">June 2026</p>
</div>
<div style="display: flex; flex-direction: column; gap: 12px;">
<div style="background: #f8f9fa; border-radius: 8px; padding: 16px;">Revenue: $42,300</div>
<div style="background: #f8f9fa; border-radius: 8px; padding: 16px;">Users: 1,847</div>
<div style="background: #f8f9fa; border-radius: 8px; padding: 16px;">Churn: 2.1%</div>
</div>
</div>
CSS grid, gradients, border-radius, nested flexbox, Google Fonts via @import or <link> tags in the HTML head. All of it renders because the engine is the same Chrome you use for development. Satori would silently drop the grid layout and fall back to block stacking.
Markdown to image in one call
Not every image starts from HTML. README files, changelogs, release notes, documentation snippets. These live in Markdown and converting them to HTML before sending to a screenshot API adds a build step most teams don't want.
Send Markdown directly via the markdown parameter and skip the conversion step entirely:
{
"markdown": "# Release v2.4.0\n\n## What's new\n\n- **Batch API** for processing up to 100 URLs in one request\n- **S3 export** sends screenshots directly to your bucket\n- Dark mode support via `dark_mode` parameter\n\n## Bug fixes\n\n- Fixed timeout handling for pages over 30 seconds\n- Resolved race condition in webhook delivery",
"width": 800,
"height": 600,
"format": "webp"
}
Internally, the Markdown is converted to styled HTML with sensible defaults (typography, code block highlighting, spacing), rendered in Chromium, and returned as an image. No external Markdown-to-HTML library needed on your end. Most screenshot APIs don't accept Markdown directly, so this saves a build step. One caveat: the default styling covers common cases, but if you need pixel-perfect control over layout and typography, send pre-styled HTML instead.
OG images and social cards from HTML templates
Generating Open Graph cards is the most common use case for HTML-to-image. Every page on a SaaS site needs a unique og:image, and designing them by hand doesn't scale past ten pages.
Build one HTML template with CSS, inject dynamic data (title, author, date, logo), and send it to the API at 1200x630 (the standard OG image size):
{
"html": "<div style='width:1200px;height:630px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#0f0c29,#302b63,#24243e);color:white;font-family:system-ui;padding:60px'><div><h1 style='font-size:48px;margin:0'>How to Cache Screenshots for Faster Page Loads</h1><p style='font-size:24px;opacity:0.7;margin-top:16px'>screenshotrun.com</p></div></div>",
"width": 1200,
"height": 630,
"format": "png"
}
That's a minimal example. In production, most teams build the template as a standalone HTML file with external CSS, inject variables server-side (Blade, Jinja, Handlebars, string interpolation), and send the rendered string to the API. The template can be as complex as you want: embedded logos via base64, custom fonts, charts rendered as inline SVG.
The same approach works for certificates (swap name, course title, date), email header images (swap subject line and preview text), and dynamic banners (swap product image and price). One template, thousands of unique outputs.
What Puppeteer costs you in production
Here's the equivalent Puppeteer code for rendering HTML to an image:
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 630 });
await page.setContent(htmlString, { waitUntil: 'networkidle0' });
const screenshot = await page.screenshot({ type: 'png' });
await browser.close();
fs.writeFileSync('output.png', screenshot);
Seven lines. Looks simple. But every line hides a production problem. puppeteer.launch() starts a 300 MB Chromium process that takes 1-3 seconds cold. page.setContent() loads web fonts over the network, and networkidle0 doesn't guarantee they've rendered. browser.close() sometimes hangs, leaking the process. Run this in a loop and memory climbs until the container gets OOM-killed.
I've seen teams spend more engineering hours maintaining their Puppeteer rendering pipeline than building the templates it renders. The build-vs-buy comparison breaks down the full operational cost. For most teams generating under 50,000 images per month, the API is cheaper than the engineering time alone.
Combining html with the rest of the toolkit
The html parameter isn't a separate product. It works with every other API feature, which is something standalone tools like htmlcsstoimage.com can't match.
Set a custom viewport to control the exact output dimensions. Use full_page to capture HTML that scrolls beyond the viewport. Inject additional css to override template styles without modifying the HTML. Add a delay if your template loads external resources that need time to render.
A practical example: generating a full-page invoice as an image with wait_for_selector to ensure a chart component finishes rendering:
{
"html": "<html><head><link rel='stylesheet' href='https://fonts.googleapis.com/css2?family=Inter:wght@400;600'></head><body>...invoice HTML with chart...</body></html>",
"width": 800,
"full_page": true,
"wait_for_selector": ".chart-rendered",
"format": "png"
}
The invoice guide covers HTML template patterns for invoices and receipts in detail, including page-break strategies when you need PDF output instead of an image.
Where htmlcsstoimage.com and wkhtmltoimage fall short
htmlcsstoimage.com (hcti.io) does one thing: HTML to image. It starts at $14/month for 200 renders and scales to $1,650/month at 500K. The rendering is real Chrome, so the output quality is comparable. But it's a single-purpose tool with no URL screenshots, no full_page capture, no cookie blocking, no viewport control, and no Markdown parameter.
wkhtmltoimage uses QtWebKit, not Chromium. That means no flexbox, no CSS grid, broken @font-face handling, and rendering artifacts on anything built after 2015. The project is effectively unmaintained. I still see tutorials recommending it, but any HTML template designed with modern CSS will render incorrectly.
Client-side libraries like html2canvas and dom-to-image re-implement CSS rendering in JavaScript Canvas. They're useful for browser-only use cases (letting users save a page section), but they're not pixel-perfect, can't handle cross-origin fonts or images, and don't run server-side. They're not a replacement for a real rendering engine.
HTML and Markdown parameter reference
Full parameter list in the API reference. Here are the HTML/Markdown-specific options:
| Parameter | Type | Default | Limit | Plan |
|---|---|---|---|---|
html | string | — | 500,000 chars | Starter+ |
markdown | string | — | 500,000 chars | Pro+ |
format | string | png | png, jpeg, webp, avif, tiff, pdf | All (PDF: Starter+) |
quality | integer | 80 | 1–100 | All |
width | integer | 1280 | 320–3840 | All |
height | integer | 800 | 200–2160 | All |
Every rendering option works with html: viewport dimensions, full-page capture, css and js injection, retina for 2x output, and delay or wait_for_selector for async content. Pick url, html, or markdown as the source, then layer on whatever the template needs.