RenderScreenshot turns your HTML and CSS into an image with one HTTP request. You send a document, the API loads it in headless Chrome at the size you ask for, and you get back a PNG, JPEG, WebP, AVIF or PDF. Use it for social cards, certificates, receipts, chart images for email, badges and any other picture you would rather design in HTML than draw by hand.
curl -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "html": "<!doctype html><body style=\"margin:0;display:grid;place-items:center;height:630px;background:#0b1220;color:#f8fafc;font:700 64px system-ui\">Hello from HTML</body>", "viewport": { "width": 1200, "height": 630 }, "output": { "format": "png" } }' \ -o card.png
That is the whole integration: no browser to install, patch or keep alive. A free account gives you a key; it is a screenshot API underneath, so the same key also captures live URLs.
What happens to your HTML
your app ──POST /v1/screenshot { html, html_base_url, viewport, output }──► RenderScreenshot API (auth, validation, cache lookup) │ cache hit? ── yes ──► stored image │ no ▼ headless Chrome on Cloudflare loads the HTML, fetches fonts and images (relative paths resolve against html_base_url) │ ▼ image stored in R2 + edge cache │ your app ◄── image bytes + X-Cache-URL (public CDN link, valid for the cache TTL) ─┘
Three things in this flow matter for HTML templates:
- The viewport is your canvas.
viewport.widthandviewport.heightset the image size in CSS pixels.viewport.scale(1 to 3) multiplies the pixels, so a 1200 x 630 card at scale 2 is a 2400 x 1260 file. - Identical requests reuse the render. The cache key is a hash of the input and all rendering options. Send the same template with the same values and the API returns the stored image. See caching.
- You get a shareable link for free. Every response carries an
X-Cache-URLheader: a CDN URL for the same image that needs no key. Put it in a database row, an email or anog:imagetag. Details are in response headers.
Relative assets: htmlbaseurl
HTML that you send has no address of its own, so <img src="/logo.svg"> has nothing to resolve against. Set html_base_url to the site that hosts your assets, and every relative link, stylesheet and script loads from there:
{ "html": "<!doctype html><html><head><link rel=\"stylesheet\" href=\"/css/card.css\"></head><body><img src=\"/img/logo.svg\" alt=\"\"><h1>Quarterly report</h1></body></html>", "html_base_url": "https://assets.example.com", "viewport": { "width": 1200, "height": 630 } }
The assets must be on the public internet. The renderer does not load addresses on private networks such as localhost or 10.x.x.x, so during development inline small assets as data: URIs. The HTML to image guide covers the other two options (a <base> element or absolute URLs) and payload size.
Web fonts that are in the image, not just in the CSS
Fonts are the most common reason a render does not match your browser. We measured this with a Google Fonts card (Inter 400 and 700, display=block) in Puppeteer 25.14 on Chrome 155:
Puppeteer waitUntil |
Font in the image? | What the browser reported |
|---|---|---|
domcontentloaded |
No, fallback font | No font faces registered yet |
load |
No, fallback font | Inter faces still loading |
networkidle0 |
Yes | Inter faces loaded |
The load event fired before the font files arrived, because Google Fonts adds the font requests from a stylesheet that is itself a network request. The API's wait.until: networkidle is the equivalent of the last row. So, for remote fonts:
- Send
"wait": { "until": "networkidle" }. Parameters are in wait options. - Or inline the font. With the woff2 files embedded as
data:URIs, the same card made no font request at all. The HTML grew from about 1.2 KB to 64 KB, and the local render was faster (100 ms vs 363 ms warm, median of 5). - Use
font-display: block(or&display=blockon the Google Fonts URL). Withswap, an early capture shows the fallback font.
{ "html": "<!doctype html><html><head><link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=block\" rel=\"stylesheet\"></head><body style=\"margin:0;font-family:Inter,sans-serif\">...</body></html>", "viewport": { "width": 1200, "height": 630 }, "wait": { "until": "networkidle", "timeout": 20 } }
Transparent PNG and WebP
Badges, stickers and overlays need an alpha channel. Set output.transparent and give the page a transparent background in your CSS (html, body { background: transparent }). It works with PNG and WebP; JPEG has no alpha channel.
{ "html": "<!doctype html><html><head><style>html,body{margin:0;background:transparent}body{display:grid;place-items:center;height:160px;font:700 34px system-ui}.b{display:flex;border-radius:22px;overflow:hidden}.b span{padding:16px 26px;color:#fff}</style></head><body><div class=\"b\"><span style=\"background:#334155\">build</span><span style=\"background:#16a34a\">passing</span></div></body></html>", "viewport": { "width": 480, "height": 160, "scale": 2 }, "output": { "format": "png", "transparent": true } }
To crop the image to the badge itself instead of the whole viewport, add "capture": { "mode": "element", "selector": ".b" }.
Code
The SDKs build the request for you. TakeOptions.html() is the HTML entry point in every SDK; see the SDK overview.
// Node.js 20+: npm install renderscreenshot import { writeFile } from 'node:fs/promises'; import { Client, TakeOptions } from 'renderscreenshot'; const client = new Client(process.env.RS_API_KEY); const image = await client.take( TakeOptions.html(cardHtml) // your template, with user values escaped .width(1200) .height(630) .format('webp') .waitFor('networkidle') ); await writeFile('card.webp', image);
# Python 3.9+ import os from renderscreenshot import Client, TakeOptions client = Client(os.environ['RS_API_KEY']) image = client.take( TakeOptions.html(card_html).width(1200).height(630).format('png').wait_for('networkidle') ) with open('card.png', 'wb') as f: f.write(image)
The SDK builders do not have methods for html_base_url or output.transparent today. For those two, send the JSON body directly:
const res = await fetch('https://api.renderscreenshot.com/v1/screenshot', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ html: badgeHtml, html_base_url: 'https://assets.example.com', viewport: { width: 480, height: 160, scale: 2 }, output: { format: 'png', transparent: true }, }), signal: AbortSignal.timeout(45_000), }); if (!res.ok) throw new Error(`render failed: ${res.status} ${await res.text()}`); const png = Buffer.from(await res.arrayBuffer()); console.log('CDN copy:', res.headers.get('x-cache-url'));
Ruby, PHP, Java and Go versions are in the HTML to image guide. If you are coming from a self-hosted renderer, the step-by-step Node.js and Python comparisons show the same templates rendered both ways.
Escape what you insert
A template turns into an injection hole the moment a user-supplied title goes into it raw. <img src=x onerror=...> would run inside the renderer, and a stray < breaks the layout. Escape every value with your language's HTML escaper (html.escape in Python, ERB::Util.html_escape in Ruby, htmlspecialchars in PHP, a five-character replace in JavaScript), and build the request body with a JSON encoder, never by string concatenation. The security best practices guide covers user-controlled templates in more depth.
Pricing
One successful render is one credit, whatever the format or size; failed renders are not charged (credits and billing). Plans as listed on the pricing page in October 2026:
| Plan | Credits per month | Price |
|---|---|---|
| Free | 150 | $0, no credit card |
| Starter | 2,500 | $10 |
| Growth | 12,000 | $40 |
| Scale | 60,000 | $100 |
A blog that generates a card for 300 posts once, then re-renders 40 cards a month after edits, fits in the Free plan after the first month and in Starter for the first one.
When an HTML to image API is the wrong tool
- You render a few images a day inside an app that already runs Chrome. A local Puppeteer
page.setContent()call is enough; our Puppeteer HTML to image tutorial shows how to do it correctly. - Your templates are simple flexbox and text, and latency is critical. A browserless renderer such as satori produces a card without starting Chrome. It supports a subset of CSS; we list what broke in the Node.js comparison.
- The image must reflect what the user sees right now in their browser, including form input. A client-side library is the only option; see HTML to image in JavaScript.
For everything else (fonts that must match, modern CSS, many templates, no browser fleet to run), get a free API key and send your first template.
Measurements on this page come from local Chrome tests on 2026-10-15 (Node 22.18, Puppeteer 25.14.0, Chrome 155, macOS on Apple M1 Pro). They show what the browser work costs. They are not API latency figures; API timing depends on region and cache state.