HTML to Image API: Render HTML and CSS to PNG

Send HTML and CSS, get a PNG, JPEG, WebP or PDF back from real Chrome. Base URLs for assets, web fonts, transparent backgrounds and signed URLs.

By Simon Chiu · Updated

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.width and viewport.height set 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-URL header: a CDN URL for the same image that needs no key. Put it in a database row, an email or an og:image tag. 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=block on the Google Fonts URL). With swap, 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.

Frequently asked questions

Which output formats does the HTML to image API support?
PNG (the default), JPEG, WebP and AVIF for images, plus PDF. Set output.format. Transparent backgrounds work with PNG and WebP.
How do I load images and stylesheets with relative paths?
Set html_base_url to the site that hosts them. The renderer resolves /img/logo.svg against that address. Absolute URLs and a base element in your head also work.
Can my HTML use Google Fonts?
Yes. Link the stylesheet in your head with display=block. In our Puppeteer tests the load event fired while Google Fonts faces were still loading, so for remote fonts wait for networkidle, or inline the font as a data URI.
Do I need Chrome on my server?
No. The API runs headless Chrome on Cloudflare's Browser Rendering platform. Your server sends JSON and receives the image bytes or a CDN URL.
Is the HTML I send stored?
The rendered image is cached so identical requests can reuse it, and it is served from the X-Cache-URL link until the cache TTL ends. Set a short cache.ttl, or cache.enabled false, for private content.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.