Written By Hanzala Saleem
Updated At June 23, 2026 | 8 min read
Generating images from HTML comes up constantly in modern web apps. Social media preview cards, invoice PDFs, email template previews, course certificates, dynamic OG images all of them start as HTML and CSS and need to end up as a raster image or document file.
The naive solution is self-hosting Puppeteer or Playwright. Works fine for a weekend prototype. Falls apart fast once you need concurrency, cloud deployment, or reliable rendering in CI/CD. Memory leaks, zombie Chrome processes, Docker image sizes north of 1 GB the infrastructure overhead quickly outweighs the value of controlling your own renderer.
A dedicated HTML-to-image API takes that headache completely off the table.
When you convert HTML to an image, you are not doing string manipulation or CSS parsing in userland. A real browser engine loads your markup, executes any JavaScript, applies stylesheets, and then captures a pixel-accurate frame exactly what a user would see. The output is either a raster image (PNG, JPG, WebP) or a vector-backed document (PDF).
This matters because simplified HTML-to-image libraries (like html2canvas on the client side, or sharp with SVG on the server) do not run a browser. They approximate rendering. Fonts render differently, gradients may shift, position: fixed elements end up in wrong places, and anything relying on JavaScript never executes. If your HTML template uses CSS Grid, custom fonts, or any dynamic data injection, you need a real browser renderer.
ScreenshotAPI runs every render inside a full, isolated Chromium instance the same engine as Google Chrome. The custom_html parameter lets you pass an arbitrary HTML string directly into that browser, bypassing any public URL entirely.
Instead of pointing the API at a live URL, you send your HTML as a string. The API mounts it in Chromium, waits for rendering to complete, and returns your chosen output format.
A few things worth knowing before you write any code:
curl -G "https://shot.screenshotapi.net/v3/screenshot" \
--data-urlencode "token=YOUR_API_KEY" \
--data-urlencode "custom_html=<div style='font-family:sans-serif;padding:40px;background:#0D0F14;color:#D38A45;font-size:48px;'>Invoice #1042</div>" \
--data-urlencode "output=image" \
--data-urlencode "file_type=png" \
--data-urlencode "width=1200" \
--data-urlencode "height=630" \
-o invoice-preview.pngThis is enough for quick testing. For production, use POST.
const fs = require("fs");
const axios = require("axios");
const html = `
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
font-family: Inter, sans-serif;
background: #0D0F14;
color: #D38A45;
display: flex;
align-items: center;
justify-content: center;
width: 1200px;
height: 630px;
}
.card {
text-align: center;
padding: 60px;
}
h1 { font-size: 56px; margin: 0; }
p { font-size: 24px; opacity: 0.75; }
</style>
</head>
<body>
<div class="card">
<h1>Your Article Title Here</h1>
<p>Published on screenshotapi.net</p>
</div>
</body>
</html>
`;
const response = await axios.post(
"https://shot.screenshotapi.net/v3/screenshot",
null,
{
params: {
token: "YOUR_API_KEY",
custom_html: html,
output: "image",
file_type: "png",
width: 1200,
height: 630,
},
responseType: "arraybuffer",
}
);
fs.writeFileSync("og-image.png", response.data);
console.log("OG image saved.");This pattern works well for dynamic OG image generation. Swap the title and publish date from your CMS at render time. No browser to manage, no Puppeteer pool to size. If you want to see how to wire this into a Next.js workflow, the blog post on automating dynamic OG image generation covers that end-to-end.
import requests
html_template = """
<html>
<head>
<style>
body {
margin: 0;
width: 794px;
font-family: Georgia, serif;
padding: 60px;
background: #fff;
}
h1 { font-size: 32px; color: #0D0F14; }
.amount { font-size: 48px; color: #D38A45; font-weight: bold; }
table { width: 100%; border-collapse: collapse; margin-top: 30px; }
td, th { padding: 12px; border-bottom: 1px solid #eee; text-align: left; }
</style>
</head>
<body>
<h1>Invoice #2041</h1>
<table>
<tr><th>Item</th><th>Qty</th><th>Price</th></tr>
<tr><td>API Plan (Pro)</td><td>1</td><td>$49.00</td></tr>
<tr><td>Overage (200 calls)</td><td>200</td><td>$4.00</td></tr>
</table>
<p class="amount">Total: $53.00</p>
</body>
</html>
"""
params = {
"token": "YOUR_API_KEY",
"custom_html": html_template,
"output": "image",
"file_type": "pdf",
"width": 794,
}
response = requests.get(
"https://shot.screenshotapi.net/v3/screenshot",
params=params
)
with open("invoice.pdf", "wb") as f:
f.write(response.content)
print("Invoice PDF saved.")Note: shorter HTML fits fine in a GET request. For multi-page invoices with rich styles, convert to a POST with a JSON body.
<?php
$html = <<<HTML
<html>
<body style="margin:0;background:#fff;font-family:sans-serif;padding:40px;">
<h1 style="color:#0D0F14;">Certificate of Completion</h1>
<p style="font-size:20px;">Awarded to <strong>Jane Doe</strong></p>
<p style="color:#D38A45;font-size:28px;">Advanced JavaScript Course</p>
</body>
</html>
HTML;
$params = http_build_query([
'token' => 'YOUR_API_KEY',
'custom_html' => $html,
'output' => 'image',
'file_type' => 'png',
'width' => 1200,
'height' => 630,
]);
$response = file_get_contents(
"https://shot.screenshotapi.net/v3/screenshot?" . $params
);
file_put_contents('certificate.png', $response);
echo "Certificate generated.\n";| Criterion | PNG | |
|---|---|---|
| Use case | OG images, social cards, thumbnails, certificates | Invoices, reports, legal documents, multi-page content |
| Transparency support | Yes (omit_background=true) |
No |
| Scalable / vector text | No (raster) | Yes |
| Page sizing | Fixed viewport (width × height) |
Auto-sized to content length |
| File size (typical) | 80 to 400 KB | 50 to 200 KB |
| Print quality | Acceptable | Superior |
| Multi-page | No | Yes (content wraps across pages) |
For OG images, social cards, and any output that will be displayed on screen: PNG. For invoices, reports, compliance docs, and anything that will be printed or archived: PDF.
A practical shortcut: if the consumer is a browser rendering the output on a webpage, use PNG. If the consumer is a human reading a document, use PDF.

| Parameter | Purpose | Example value |
|---|---|---|
| custom_html | Your HTML string to render | Full HTML document string |
| file_type | Output format | png, pdf, jpg, webp |
| width | Viewport width in pixels (max 7680) | 1200 |
| height | Viewport height in pixels (max 4320) | 630 |
| output | Response format | image or json |
| full_page | Capture full document height | true |
| retina | 2x pixel density for sharp output | true |
| omit_background | Transparent PNG background | true (PNG only) |
| delay | Wait time before capture (ms) | 1000 |
| wait_for_event | Trigger timing | load, networkidle, domcontentloaded |
For a complete parameter reference, the ScreenshotAPI documentation covers all 75+ options.
Dynamic OG images. Generate a 1200x630 PNG for every blog post, product page, or user profile. Inject the title, author, and publication date from your CMS into the HTML template. Links with custom preview images consistently get 2-3 times more clicks on social. The OG image automation guide goes deeper into the full pipeline.
Invoice and receipt PDFs. Build an HTML/CSS invoice template once. At checkout or billing time, inject customer details and line items, send the string to the API, and return the PDF directly to the user. No PDF libraries, no font embedding headaches.
Email preview images. Render your HTML email template as a PNG so you can include a visual preview in documentation, approval flows, or testing dashboards.
Course completion certificates. Dynamic certificates with the recipient's name, course title, and date are generated on demand, returned as PNG or PDF, delivered via webhook, or stored to S3.
Social media cards. Generate platform-specific image sizes (Twitter, LinkedIn, Open Graph) from a single HTML template by varying the width and height parameters per request.
Always set explicit dimensions. The default viewport (1680x867) is fine for website screenshots, but HTML templates usually target specific output sizes. Set width and height explicitly in every request for consistent results.
Send large HTML via POST. URLs have practical length limits. A complex invoice template with inline CSS easily exceeds what a GET query string can safely carry. Use POST with Content-Type: application/json for anything substantial.
Enable retina=true for presentations and print. A 2x render costs slightly more processing time but produces output that looks sharp on HiDPI displays and in print. For OG images, retina is usually overkill; 1x at 1200x630 is what social platforms expect. For certificates and reports, enable it.
Never put secrets in custom_html. The HTML string is sent as a request parameter. Do not embed API keys, tokens, or personal data in the template itself. Inject only display values (names, amounts, dates), not credentials.
Authenticate server-side. Your ScreenshotAPI token should only ever be used from your backend. Exposing it in client-side JavaScript lets anyone generate renders billed to your account.
Validate HTML template inputs. If user-supplied data is injected into your HTML template (a customer name on a certificate, for example), sanitize it before injection. Unsanitized input could lead to XSS in the rendered output if the HTML is later displayed in a browser context.
Sending HTML without a doctype or viewport. A bare fragment like <div>Hello</div> will render, but the browser applies quirks mode defaults. Wrap templates in a proper <!DOCTYPE html> document with an explicit <meta name="viewport"> for predictable results.
Forgetting to URL-encode the HTML. If you send custom_html as a GET parameter, the HTML string must be URL-encoded. Unencoded ampersands, angle brackets, and equals signs will break the query string parsing. Use your HTTP library's built-in encoding rather than doing it manually.
Using output=json when you want the file directly. The default response format is JSON, which returns a URL pointing to the file. If you want the raw binary image in the response body (so you can stream it to the user or write it to disk immediately), add output=image.
Not accounting for font loading time. If your template uses custom fonts, the first render will be slightly slower while fonts load. Subsequent renders with caching enabled will be faster. For templates with external resources, always use wait_for_event=networkidle.

Yes. The custom_html parameter accepts any HTML string and renders it directly in Chromium without requiring a live URL. This is exactly the right approach for dynamically generated templates (invoices, certificates, OG images) where the HTML never exists as a hosted page.
There is no hard documented limit on the HTML string length, but GET requests have practical URL length limits (typically 8 KB to 32 KB depending on server and proxy). For anything beyond a simple template, use a POST request to avoid truncation. The docs confirm both GET and POST are supported.
It works out of the box. ScreenshotAPI renders inside full Chromium, which supports all modern CSS, including Grid, Flexbox, and custom properties. No special parameters needed; write your CSS normally.
Yes. For PDF output, leave height unset. The API generates a continuous PDF sized to the full document height, so nothing gets cut off. This is specifically useful for invoices and reports where the content length varies per document.