# HTML to Image API: Render HTML/CSS Templates to PNG (`bossless/html-to-image`) Actor

Render HTML/CSS to image: PNG, JPG, WebP or PDF. Fill a template's {{placeholders}} from a list of data rows and get one image per row: OG images, social cards, certificates, badges. Transparent backgrounds, retina scale, element clipping. No API key. Pay per image.

- **URL**: https://apify.com/bossless/html-to-image.md
- **Developed by:** [Alexander Daskalov](https://apify.com/bossless) (community)
- **Categories:** Developer tools, Automation, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 image rendereds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## HTML to Image API: Render HTML/CSS Templates to PNG

**Turn HTML and CSS into PNG, JPG, WebP or PDF files, one per row of your data.** Write one template with `{{placeholders}}`, give it a list of rows (blog posts, attendees, products…), and get a finished image for every row, each with its own download link:

- 🖼️ **Templates + data rows**: `{{title}}`, `{{author}}`, `{{user.name}}` are filled from each row, so 500 rows make 500 images in one run.
- 📦 **PNG, JPEG, WebP or PDF**: pick the format, size and quality.
- 🔍 **Retina scale**: render at 2× or 3× for crisp images on high-DPI screens.
- ✂️ **Element clipping**: capture just one element (a card, a badge) with a CSS selector, or the full page.
- 🫥 **Transparent backgrounds**: PNG and WebP badges and stickers with no white box.
- ⚙️ **A real browser**: headless Chromium runs your JavaScript (charts work), loads Google Fonts and public images, and has emoji and Chinese/Japanese/Korean fonts built in.

No API key, no account to connect, no setup.

### What can you do with it?

- **Open Graph and social share images:** one branded card per blog post, product or listing for Facebook, LinkedIn, X and Slack previews.
- **Certificates and badges from an attendee list:** course certificates, event badges and name tags, one file per person.
- **Price tags and product labels:** shelf labels and promo tags from a product sheet.
- **Email banners:** personalised header images per customer or campaign.
- **Charts rendered from HTML/JS:** draw a chart with your favourite JavaScript library and save it as an image for reports and emails.
- **HTML invoices and receipts to PDF:** turn an invoice template and a list of orders into PDFs.
- **Dynamic images for AI agents:** let your assistant design an image in HTML and get back a PNG link.

### How it works

1. You write the HTML (a snippet or a full document) and, if you like, extra CSS.
2. Each object in **Data rows** fills the template's placeholders and becomes one image.
3. Every file is saved in the run's key-value store, and the dataset gets one record per image with its download link.

A short template:

```html
<div class="card">
  <p class="site">{{site}}</p>
  <h1>{{title}}</h1>
  <p>{{author}} · {{date}}</p>
</div>
```

and its data rows:

```json
[
  {"site": "blog.example.com", "title": "How we cut our cloud bill by 40%", "author": "Maria Lopez", "date": "Oct 2026", "fileName": "cloud-bill"},
  {"site": "blog.example.com", "title": "10 lessons from shipping every week", "author": "Sam Chen", "date": "Oct 2026", "fileName": "shipping-weekly"}
]
```

give two images, `cloud-bill.png` and `shipping-weekly.png`.

**Placeholder rules:**

| Placeholder | What it inserts |
|---|---|
| `{{name}}` | The value as text, HTML-escaped, so `<`, `>` and `&` in your data show up as typed |
| `{{{name}}}` | The value as raw HTML, e.g. `"<b>Gold</b> member"` or an `<img>` tag |
| `{{user.name}}`, `{{items.0}}` | Nested values: object fields and list items (counting from 0) |
| a field the row doesn't have | Renders empty, and is listed in the record's `missingFields`, so a typo in a field name is easy to spot |

A `null` value also renders empty. With no data rows, the HTML is rendered once as it is.

### Output options

| Option | Default | What it does |
|---|---|---|
| **Format** (`format`) | `png` | `png`, `jpeg`, `webp` or `pdf` |
| **Width / Height** (`width`, `height`) | 1200 × 630 | Viewport size in CSS pixels, 16 to 4096. For PDF, the page size |
| **Scale** (`scale`) | 1 | Device pixel ratio, 1 to 3. 2 = retina: a 1200×630 card becomes 2400×1260 pixels |
| **Full page** (`fullPage`) | off | Capture the whole content height, not just the viewport. For PDF, one page as tall as the content |
| **Clip to element** (`selector`) | none | Capture only the first element matching a CSS selector, e.g. `#card` (images only) |
| **Transparent background** (`transparentBackground`) | off | No white background, for PNG and WebP. Your HTML must not set its own background |
| **Quality** (`quality`) | 90 | JPEG and WebP quality, 1 to 100 |
| **Extra wait** (`waitMs`) | 0 | Wait longer after the page, images and fonts have loaded, e.g. for chart animations (up to 10,000 ms) |
| **File name field** (`fileNameField`) | `fileName` in the example | Which data field names the file. Names are cleaned up to safe characters and duplicates get `-2`, `-3`…; without it, files are `image-1`, `image-2`… |

### How much does it cost?

Pay per image. No monthly fee.

| Event | Price (FREE/BRONZE) | Lower on paid plans |
|---|---|---|
| Image rendered (PNG, JPEG, WebP or PDF, saved with its link) | **$0.003** / image | down to $0.0025 |
| Actor start | $0.00005 / run | — |

1,000 images cost **$3**. You pay only for images actually rendered and saved. Nothing is charged for an image that fails to render.

**Cap your spend:** set a **maximum cost per run**. When the budget runs out, no more images are rendered.

### How to render HTML to images

1. Click **Try for free**. The example input renders two blog post cards.
2. Paste your own HTML into **HTML template** and your rows into **Data rows**.
3. Optional: change the size, format, scale or file name field.
4. Run, then open the **Images** view and click a download link, or open the **Image files** tab.

#### Input example

```json
{
  "html": "<div class=\"badge\"><b>{{name}}</b><br>{{role}}</div>",
  "css": ".badge { width: 400px; padding: 24px; border-radius: 16px; background: #111827; color: #fff; font: 24px sans-serif; }",
  "data": [
    {"name": "Ana Petrova", "role": "Speaker", "fileName": "ana"},
    {"name": "Leo Martin", "role": "Attendee", "fileName": "leo"}
  ],
  "selector": ".badge",
  "scale": 2,
  "transparentBackground": true,
  "fileNameField": "fileName"
}
```

### Output

One record per image:

```json
{
 "index": 0,
 "fileName": "cloud-bill.png",
 "url": "https://api.apify.com/v2/key-value-stores/<store-id>/records/cloud-bill.png",
 "format": "png",
 "width": 1200,
 "height": 630,
 "bytes": 48211,
 "data": {"site": "blog.example.com", "title": "How we cut our cloud bill by 40%", "author": "Maria Lopez", "date": "Oct 2026", "fileName": "cloud-bill"},
 "missingFields": [],
 "renderMs": 310
}
```

**Getting the files:**

- `url` is the download link of the file. If your account keeps storages private, add `?token=YOUR_TOKEN` to it.
- All files are also in the run's **key-value store** (the **Image files** tab in Console, or the API), under their `fileName`.
- For PDFs, `url` points to a `.pdf` file and `width`/`height` are the page size in CSS pixels.
- `width` and `height` of images are real pixels, so they include the scale.

### How fast is it?

Images are rendered 4 at a time in one browser. Measured on Apify: 200 images (1200×630 PNG) in 330 seconds with 1024 MB of memory, about 1.6 seconds per image (around 36 a minute).

### Security and limits

- HTML up to **1 MB**, up to **5,000 data rows** per run, and **30 seconds** per image.
- Pages can load public images, fonts and scripts from the internet.
- Page requests (HTTP and HTTPS) to private networks, such as localhost, internal IPs and cloud metadata addresses, are blocked, and local files aren't loaded. WebSockets and WebRTC are switched off, and audio and video aren't loaded.
- An image that fails (a timeout, or a selector that matches nothing) is skipped, logged with its row number and not charged; the other rows carry on. If 20 images fail in a row, the run stops starting new ones, since the template is most likely broken.

### Use it with AI agents (MCP)

```
https://mcp.apify.com?tools=bossless/html-to-image
```

Ask your assistant "make an Open Graph image for each of these blog posts", and it writes the HTML, runs this actor and gives you the image links.

### Use the API from code

```bash
curl -X POST "https://api.apify.com/v2/acts/bossless~html-to-image/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"html": "<h1 style=\"font:64px sans-serif\">{{title}}</h1>", "data": [{"title": "Hello"}], "width": 800, "height": 400}'
```

```python
from pathlib import Path
from apify_client import ApifyClient

client = ApifyClient("YOUR_TOKEN")
run = client.actor("bossless/html-to-image").call(run_input={
    "html": "<div style='padding:40px;font:48px sans-serif'>Certificate for <b>{{name}}</b></div>",
    "data": [{"name": "Ana Petrova"}, {"name": "Leo Martin"}],
    "width": 1600,
    "height": 1100,
    "format": "pdf",
})
store = client.key_value_store(run["defaultKeyValueStoreId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    record = store.get_record_as_bytes(item["fileName"])
    Path(item["fileName"]).write_bytes(record["value"])
    print(item["fileName"], item["bytes"], "bytes", item["missingFields"])
```

### FAQ

**Can I use Google Fonts?** Yes. Add the font's `<link>` tag from Google Fonts to your HTML (or an `@import` in the CSS) and use the font family. Any public font file works the same way.

**Does JavaScript run? Do charts work?** Yes. Scripts run in a real Chromium browser, so chart libraries loaded from a public CDN draw before the capture. If a chart animates, set **Extra wait** to a few hundred milliseconds.

**Do emoji and Chinese, Japanese or Korean text work?** Yes. Emoji and CJK fonts are installed, so text in any language renders instead of showing boxes.

**How do I get a transparent PNG?** Turn on **Transparent background**, choose PNG (or WebP), and make sure your HTML doesn't set a background on `body` or the element you capture. Clipping to the element with **Clip to element** gives a tight image with no empty margin.

**How do I get retina images?** Set **Scale** to 2 (or 3). The layout stays the same and the image gets 2× (or 3×) the pixels.

**What page size does a PDF have?** The **Width** × **Height** you set, in CSS pixels, with backgrounds printed. With **Full page** on, the page is as tall as the content.

**How do I get the files?** Every record has a `url` download link, and all files are in the run's key-value store under their `fileName`. See [Output](#output).

**What if a placeholder isn't in my data?** It renders empty and is listed in `missingFields`, so you can find typos without opening every image.

**Is my HTML stored?** Your input and the rendered files stay in your own Apify run storage. Nothing is sent anywhere else, except the requests your own page makes for its images, fonts and scripts.

**Is this an official product of any provider?** No. This is an independent tool and is not affiliated with or endorsed by Google, the Chromium project or any screenshot service.

### Related tools

- [Handwriting to Text](https://apify.com/bossless/handwriting-to-text): photos, scans and PDFs of handwritten notes, letters and forms turned into text, line by line, in any language.

# Actor input Schema

## `html` (type: `string`):

An HTML snippet or a full HTML document (up to 1 MB). Use {{field}} to insert a value from each data row (HTML-escaped), {{{field}}} to insert raw HTML, and dotted paths such as {{user.name}} or {{items.0}} for nested values. JavaScript runs, and public images, fonts (e.g. Google Fonts via <link>) and scripts load.

## `css` (type: `string`):

Optional CSS added in a <style> tag in the page's <head>. Handy for keeping styles out of the template.

## `data` (type: `array`):

A list of objects, one image per object; each fills the template's placeholders. Leave empty to render the HTML once as it is. Up to 5,000 rows per run.

## `width` (type: `integer`):

Viewport width in CSS pixels. For PDF, the page width.

## `height` (type: `integer`):

Viewport height in CSS pixels. For PDF, the page height.

## `fullPage` (type: `boolean`):

Capture the whole content height instead of just the viewport. For PDF, makes one page as tall as the content.

## `selector` (type: `string`):

Optional. Capture only the first element matching this CSS selector, e.g. #card or .badge. An image whose selector matches nothing fails and isn't charged. Not used for PDF.

## `scale` (type: `number`):

1 = normal, 2 = retina (a 1200×630 card becomes 2400×1260 pixels), up to 3.

## `format` (type: `string`):

File format of each output.

## `quality` (type: `integer`):

JPEG and WebP quality, 1 to 100.

## `transparentBackground` (type: `boolean`):

Leave the page background transparent instead of white (PNG and WebP). The HTML must not set its own background.

## `waitMs` (type: `integer`):

Extra time to wait after the page, its images and fonts have loaded, e.g. for chart animations. Up to 10,000 ms.

## `fileNameField` (type: `string`):

Data row field used as the file name (cleaned up to safe characters; duplicates get -2, -3…). Rows without it, or an empty setting, are named image-1, image-2…

## Actor input object example

```json
{
  "html": "<style>body{margin:0}</style><div style=\"width:1200px;height:630px;box-sizing:border-box;padding:72px;background:linear-gradient(135deg,#0ea5e9,#6366f1);color:#fff;font-family:Inter,Segoe UI,Arial,sans-serif;display:flex;flex-direction:column;justify-content:space-between\"><div style=\"font-size:28px;opacity:.85\">{{site}}</div><div style=\"font-size:64px;font-weight:800;line-height:1.1\">{{title}}</div><div style=\"font-size:30px\">{{author}} · {{date}}</div></div>",
  "data": [
    {
      "site": "blog.example.com",
      "title": "How we cut our cloud bill by 40%",
      "author": "Maria Lopez",
      "date": "Oct 2026",
      "fileName": "cloud-bill"
    },
    {
      "site": "blog.example.com",
      "title": "10 lessons from shipping every week",
      "author": "Sam Chen",
      "date": "Oct 2026",
      "fileName": "shipping-weekly"
    }
  ],
  "width": 1200,
  "height": 630,
  "fullPage": false,
  "scale": 1,
  "format": "png",
  "quality": 90,
  "transparentBackground": false,
  "waitMs": 0,
  "fileNameField": "fileName"
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `files` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "html": "<style>body{margin:0}</style><div style=\"width:1200px;height:630px;box-sizing:border-box;padding:72px;background:linear-gradient(135deg,#0ea5e9,#6366f1);color:#fff;font-family:Inter,Segoe UI,Arial,sans-serif;display:flex;flex-direction:column;justify-content:space-between\"><div style=\"font-size:28px;opacity:.85\">{{site}}</div><div style=\"font-size:64px;font-weight:800;line-height:1.1\">{{title}}</div><div style=\"font-size:30px\">{{author}} · {{date}}</div></div>",
    "data": [
        {
            "site": "blog.example.com",
            "title": "How we cut our cloud bill by 40%",
            "author": "Maria Lopez",
            "date": "Oct 2026",
            "fileName": "cloud-bill"
        },
        {
            "site": "blog.example.com",
            "title": "10 lessons from shipping every week",
            "author": "Sam Chen",
            "date": "Oct 2026",
            "fileName": "shipping-weekly"
        }
    ],
    "fileNameField": "fileName"
};

// Run the Actor and wait for it to finish
const run = await client.actor("bossless/html-to-image").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "html": "<style>body{margin:0}</style><div style=\"width:1200px;height:630px;box-sizing:border-box;padding:72px;background:linear-gradient(135deg,#0ea5e9,#6366f1);color:#fff;font-family:Inter,Segoe UI,Arial,sans-serif;display:flex;flex-direction:column;justify-content:space-between\"><div style=\"font-size:28px;opacity:.85\">{{site}}</div><div style=\"font-size:64px;font-weight:800;line-height:1.1\">{{title}}</div><div style=\"font-size:30px\">{{author}} · {{date}}</div></div>",
    "data": [
        {
            "site": "blog.example.com",
            "title": "How we cut our cloud bill by 40%",
            "author": "Maria Lopez",
            "date": "Oct 2026",
            "fileName": "cloud-bill",
        },
        {
            "site": "blog.example.com",
            "title": "10 lessons from shipping every week",
            "author": "Sam Chen",
            "date": "Oct 2026",
            "fileName": "shipping-weekly",
        },
    ],
    "fileNameField": "fileName",
}

# Run the Actor and wait for it to finish
run = client.actor("bossless/html-to-image").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "html": "<style>body{margin:0}</style><div style=\\"width:1200px;height:630px;box-sizing:border-box;padding:72px;background:linear-gradient(135deg,#0ea5e9,#6366f1);color:#fff;font-family:Inter,Segoe UI,Arial,sans-serif;display:flex;flex-direction:column;justify-content:space-between\\"><div style=\\"font-size:28px;opacity:.85\\">{{site}}</div><div style=\\"font-size:64px;font-weight:800;line-height:1.1\\">{{title}}</div><div style=\\"font-size:30px\\">{{author}} · {{date}}</div></div>",
  "data": [
    {
      "site": "blog.example.com",
      "title": "How we cut our cloud bill by 40%",
      "author": "Maria Lopez",
      "date": "Oct 2026",
      "fileName": "cloud-bill"
    },
    {
      "site": "blog.example.com",
      "title": "10 lessons from shipping every week",
      "author": "Sam Chen",
      "date": "Oct 2026",
      "fileName": "shipping-weekly"
    }
  ],
  "fileNameField": "fileName"
}' |
apify call bossless/html-to-image --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,bossless/html-to-image"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/d81sVQsDez4Nqzg52/builds/4vCOp4wY1XRFYMEfl/openapi.json
