# HTML to PDF: Templates, Invoices & Arabic RTL (`maliksaid/html-to-pdf`) Actor

HTML to PDF converter for templates and Markdown. Merge JSON, CSV or dataset rows with Handlebars to make one PDF per row: invoices, certificates, reports. Page size, margins, header, footer, page numbers, Arabic RTL fonts. Public PDF links.

- **URL**: https://apify.com/maliksaid/html-to-pdf.md
- **Developed by:** [Malik Said](https://apify.com/maliksaid) (community)
- **Categories:** Developer tools, Automation, Other
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.40 / 1,000 pdf generateds

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 PDF: Templates, Invoices & Arabic RTL

Turn an HTML or Markdown template into PDF files. Add a list of data rows, and the Actor makes one PDF per row. Use it for invoices, receipts, certificates, letters and reports. Each PDF gets a public download link in the dataset.

The template uses Handlebars fields such as `{{customer}}`. Rows can come from JSON, pasted CSV, an Apify dataset or a URL. You choose the page size, margins, header, footer and page numbers. Arabic and other right-to-left documents print correctly with bundled Arabic fonts.

### What you can use it for

- Invoices and receipts from an order export, one PDF per order.
- Course certificates or badges from a list of names.
- Monthly reports with tables and charts, in English or Arabic.
- Letters, quotes and contracts from a spreadsheet.
- Markdown notes or documentation as clean printable PDFs.
- PDFs from the output of another Actor: pass its dataset ID.

### How to use it

1. Paste your template in **Template**. HTML gives full control with your own CSS. For Markdown, set **Template type** to Markdown.
2. Add your rows in **Data rows (JSON)**, **Data rows (CSV)**, **Apify dataset ID** or **Data URL**.
3. Set **File name**, for example `invoice-{{number}}`.
4. Pick the page size, margins and an optional header and footer.
5. Optional: turn on **Also save one ZIP of all PDFs** or **Also save one merged PDF** to get every document in one download or one printable file. Both are free.
6. Run. Open the PDF links in the dataset, or use the ZIP and merged PDF links in the `BUNDLES` record.

Leave the template empty to try the built-in sample: three two-language invoices.

### Input example

```json
{
    "template": "<h1>Invoice {{number}}</h1><p>Bill to: {{customer}}</p><table>{{#each items}}<tr><td>{{name}}</td><td>{{formatCurrency (multiply qty price) ../currency}}</td></tr>{{/each}}</table><p>Total: {{formatCurrency (sum items \"qty\" \"price\") currency}}</p>",
    "data": [
        { "number": "INV-1001", "customer": "Example Trading LLC", "currency": "USD", "items": [{ "name": "Design work", "qty": 10, "price": 45 }] }
    ],
    "fileName": "invoice-{{number}}",
    "pageFormat": "A4",
    "marginTop": "20mm",
    "footerTemplate": "<div style=\"text-align:center\">Invoice {{number}}, page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>"
}
```

### Template fields and helpers

Handlebars fills `{{field}}` with the value from the row and escapes HTML in it. Use `{{{field}}}` with three braces only for HTML you trust. Loops (`{{#each items}}`), conditions (`{{#if paid}}`) and nested fields (`{{customer.name}}`) work.

| Helper | Example | Result |
|---|---|---|
| `formatNumber` | `{{formatNumber 1234.5 decimals=2 locale="de-DE"}}` | 1.234,50 |
| `formatCurrency` | `{{formatCurrency 99 "SAR" locale="ar-SA"}}` | ٩٩٫٠٠ ر.س.‏ |
| `formatDate` | `{{formatDate date style="long"}}` | October 1, 2026 |
| `sum` | `{{sum items "qty" "price"}}` | Sum of qty x price |
| `add`, `subtract`, `multiply`, `divide` | `{{multiply qty price}}` | Number |
| `eq` | `{{#if (eq status "paid")}}` | true or false |
| `upper`, `lower`, `default` | `{{default note "None"}}` | Text |
| `json` | `{{{json labels}}}` | JSON for a script, such as a chart |

Every row also gets `_rowNumber` (from 1), `_index` (from 0) and `_runDate` (today, YYYY-MM-DD).

### Arabic and right-to-left documents

- **Text direction** Auto keeps the `dir` of your HTML. Without one, the Actor picks right to left when most letters are Arabic or Hebrew. You can also force left to right or right to left.
- Five fonts are bundled: Noto Sans, Noto Sans Arabic, Noto Naskh Arabic, Cairo and Amiri, in regular and bold. Use them by name in your CSS, for example `font-family: 'Cairo'`. **Default font** sets the font for text your CSS does not style.
- The default page-number footer is in Arabic for right-to-left pages: صفحة 1 من 3.
- Use CSS logical properties such as `text-align: start` so one template works in both directions.

### Images, fonts and scripts

The page has no network access by default. This keeps your data private and stops a template from reaching other systems. To load images, web fonts, CSS or chart libraries, add their hosts to **Allowed asset hosts**, for example `cdn.jsdelivr.net`, or `fonts.googleapis.com` and `fonts.gstatic.com` for Google Fonts. Subdomains are included.

- Requests to other hosts are blocked. The row counts them in `blockedRequests` and lists the hosts in `blockedHosts`.
- Local and private network addresses are always refused, even when allowed.
- Each asset is fetched once per run and reused for every PDF, up to 15 MB per file.
- Images in the header and footer must be `data:` URLs. This is a Chromium rule.
- For charts, turn off animation and fixed-size canvases print best (Chart.js: `animation: false, responsive: false`).
- When a script fills the page late, set **Wait for element** to a CSS selector, for example `#chart[data-ready]`. Each PDF prints as soon as that element exists, up to 20 s per row. A row whose element never appears is saved as failed with a reason and is free.

### Output

One dataset row per data row. Generated PDFs are saved in the run's key-value store.

With the ZIP or merged PDF options on, the key-value store also holds `all-pdfs.zip` and `all-pdfs.pdf` (or numbered parts for very large runs). The `BUNDLES` record lists each file with its link, file count, size and, for the merged PDF, its page count. They are not charged and add no dataset rows.

| Field | Meaning |
|---|---|
| `rowNumber` | Position of the data row, from 1. Rows can finish out of order |
| `status` | `generated`, or `failed` with a `reason` |
| `fileName` | The name from your File name template |
| `fileKey` | Key in the key-value store. Letters other than a to z become dashes |
| `fileUrl` | Public link to download the PDF |
| `fileSizeBytes`, `pageCount` | Size and number of pages |
| `pageFormat`, `landscape`, `direction` | How the PDF was printed |
| `blockedRequests`, `blockedHosts`, `assetErrors` | Network requests that were blocked or failed |
| `renderMs` | Time to render and save the PDF |

The row does not copy your data row.

Sample record from a test run of the default input:

```json
{
    "id": "6c5e00e6325074a4",
    "rowNumber": 1,
    "status": "generated",
    "reason": null,
    "fileName": "invoice-INV-1001.pdf",
    "fileKey": "invoice-INV-1001.pdf",
    "fileUrl": "https://api.apify.com/v2/key-value-stores/y70ojoszSBrciNR8w/records/invoice-INV-1001.pdf?signature=1nveIAM9YRl1h0XwFpPZz",
    "fileSizeBytes": 21569,
    "pageCount": 1,
    "pageFormat": "A4",
    "landscape": false,
    "direction": "ltr",
    "templateType": "html",
    "blockedRequests": 0,
    "blockedHosts": [],
    "assetErrors": [],
    "renderMs": 451,
    "scrapedAt": "2026-10-22T09:18:42.527Z"
}
```

### Limits and memory

- One PDF may take up to 60 seconds. A slower page is retried twice, then saved as a free failed row.
- The Actor prints one PDF at a time. At the default 1,024 MB, a 1,000-invoice run finished with no failures in tests.
- Long documents need more memory. In tests, 100-page tables printed at 1,024 MB. 200-page tables needed 2,048 MB. Set the memory in the run options.
- A PDF that runs out of memory is saved as a free failed row. The other rows go on.
- Tagged PDF (for screen readers) makes files much larger. It is off by default.
- A ZIP part holds up to 1,000 PDFs or 1 GB. A merged PDF part holds up to 100 MB of PDFs. Larger runs get numbered parts. If a bundle cannot be built, each PDF keeps its own link and the run still succeeds.

### Pricing

You pay a small start fee per run and a fee for each PDF generated. Failed rows are free. See the Pricing tab for the current prices.

### Related Actor

To capture live web pages as screenshots or PDFs, use [Website Screenshot & PDF Capture](https://apify.com/maliksaid/website-screenshot-pdf). This Actor is for documents you build from your own templates and data.

### Support

Report a problem in the Issues tab. Include the run link and, if you can, a small template that shows the problem.

# Actor input Schema

## `template` (type: `string`):

Your HTML or Markdown with Handlebars fields such as {{customer}}. Each data row fills the fields and makes one PDF. Loops ({{#each items}}), conditions ({{#if paid}}) and helpers (formatNumber, formatCurrency, formatDate, sum, multiply, json) work. Leave empty to use the sample invoice.

## `templateType` (type: `string`):

HTML gives full control with your own CSS. Markdown is turned into a clean printable page with headings and tables.

## `templateUrl` (type: `string`):

Optional. Load the template from a public URL instead, for example a raw GitHub file. Used only when the Template field is empty. Local and private network addresses are refused.

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

A JSON list of objects. Each object fills the template once and makes one PDF. Nested lists such as invoice lines work with {{#each}}.

## `csvData` (type: `string`):

Paste CSV text with a header row. Each line makes one PDF. Comma or semicolon separators and quoted values work. Column names become the field names.

## `datasetId` (type: `string`):

Use the items of an Apify dataset as data rows, for example the output of another Actor. Read page by page, so large datasets work.

## `dataUrl` (type: `string`):

A public URL that returns a JSON list or CSV text, up to 10 MB. Local and private network addresses are refused.

## `fileName` (type: `string`):

Name for each PDF, with Handlebars fields, for example invoice-{{number}}. Letters other than a to z become dashes in the storage key. Default: document-{{\_rowNumber}}.

## `pageFormat` (type: `string`):

Paper size of each PDF.

## `landscape` (type: `boolean`):

Print the pages sideways.

## `zipAll` (type: `boolean`):

Also save one ZIP file with every generated PDF, free of charge. A ZIP part holds up to 1,000 PDFs or 1 GB; larger runs get several parts. Links are in the BUNDLES record of the key-value store.

## `mergeAll` (type: `boolean`):

Also save one PDF with all generated PDFs in row order, for printing, free of charge. A part holds up to 100 MB of PDFs; larger runs get several parts. Links are in the BUNDLES record.

## `marginTop` (type: `string`):

A number with mm, cm, in or px, for example 15mm. Headers and footers print inside the top and bottom margins.

## `marginRight` (type: `string`):

A number with mm, cm, in or px, for example 15mm. Headers and footers print inside the top and bottom margins.

## `marginBottom` (type: `string`):

A number with mm, cm, in or px, for example 15mm. Headers and footers print inside the top and bottom margins.

## `marginLeft` (type: `string`):

A number with mm, cm, in or px, for example 15mm. Headers and footers print inside the top and bottom margins.

## `headerTemplate` (type: `string`):

Optional HTML printed at the top of every page. Handlebars fields work. Chromium fills <span class="pageNumber"></span>, <span class="totalPages"></span>, <span class="date"></span> and <span class="title"></span>. Images must be data: URLs.

## `footerTemplate` (type: `string`):

Optional HTML printed at the bottom of every page. Same rules as the header. When empty and Page numbers is on, the footer says Page 1 of 3 (in Arabic for right-to-left documents).

## `pageNumbers` (type: `boolean`):

Add a Page X of Y footer when no footer HTML is given.

## `direction` (type: `string`):

Auto keeps the dir attribute of your HTML. Without one, it picks right to left when most letters are Arabic or Hebrew.

## `language` (type: `string`):

Optional lang attribute for the page, for example ar, en or fa. Helps hyphenation and font choice.

## `defaultFont` (type: `string`):

Font for text your CSS does not style. All five bundled fonts (Noto Sans, Noto Sans Arabic, Noto Naskh Arabic, Cairo, Amiri) can also be named in your own CSS.

## `printBackground` (type: `boolean`):

Print background colours and images.

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

Zoom of the page content, from 0.1 to 2.

## `preferCssPageSize` (type: `boolean`):

Use the size from an @page rule in your CSS instead of Page size.

## `taggedPdf` (type: `boolean`):

Add a structure tree for screen readers. Files get much larger: a 200-page table went from about 1 MB to 11 MB in tests.

## `allowedAssetHosts` (type: `array`):

Hosts your template may load images, fonts, CSS or scripts from, for example cdn.jsdelivr.net or fonts.googleapis.com and fonts.gstatic.com. Subdomains are included. All other network requests are blocked, and private addresses are always refused.

## `enableJavaScript` (type: `boolean`):

Run scripts in the template, for example a chart library. Scripts cannot reach the network except the allowed hosts.

## `waitForSelector` (type: `string`):

Print only after an element matching this CSS selector appears, for example #chart\[data-ready]. Waits up to 20 s per row. A row whose element never appears fails with a reason and is not charged. Extra wait applies after it.

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

Wait this long after the page and fonts load, for example for chart animations. Up to 10,000 ms.

## `maxPdfs` (type: `integer`):

Stop after this many PDFs. The default keeps test runs short. Set 0 for no limit.

## Actor input object example

```json
{
  "template": "<!doctype html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>Invoice {{number}}</title>\n<style>\n  body { font-size: 11pt; color: #102a43; }\n  .top { display: flex; justify-content: space-between; align-items: flex-start; }\n  h1 { margin: 0; font-size: 22pt; }\n  .ar { font-family: 'Noto Sans Arabic', sans-serif; direction: rtl; }\n  table { width: 100%; border-collapse: collapse; margin-top: 18px; }\n  th, td { border-bottom: 1px solid #d9e2ec; padding: 8px; text-align: start; }\n  th { background: #f0f4f8; }\n  .num { text-align: end; }\n  .total td { font-weight: bold; border-top: 2px solid #102a43; }\n</style>\n</head>\n<body>\n  <div class=\"top\">\n    <div>\n      <h1>Invoice</h1>\n      <div>No. {{number}}</div>\n      <div>Date: {{formatDate date style=\"long\"}}</div>\n    </div>\n    <div class=\"ar\">\n      <h1>فاتورة</h1>\n      <div>رقم {{number}}</div>\n      <div>التاريخ: {{formatDate date style=\"long\" locale=\"ar-SA-u-ca-gregory\"}}</div>\n    </div>\n  </div>\n  <p><strong>Bill to:</strong> {{customer}}</p>\n  <table>\n    <thead><tr><th>Item</th><th class=\"num\">Qty</th><th class=\"num\">Unit price</th><th class=\"num\">Amount</th></tr></thead>\n    <tbody>\n      {{#each items}}\n      <tr><td>{{name}}</td><td class=\"num\">{{qty}}</td><td class=\"num\">{{formatCurrency price ../currency}}</td><td class=\"num\">{{formatCurrency (multiply qty price) ../currency}}</td></tr>\n      {{/each}}\n      <tr class=\"total\"><td colspan=\"3\">Total / الإجمالي</td><td class=\"num\">{{formatCurrency (sum items \"qty\" \"price\") currency}}</td></tr>\n    </tbody>\n  </table>\n</body>\n</html>",
  "templateType": "html",
  "data": [
    {
      "number": "INV-1001",
      "date": "2026-10-01",
      "customer": "Example Trading LLC",
      "currency": "USD",
      "items": [
        {
          "name": "Design work",
          "qty": 10,
          "price": 45
        },
        {
          "name": "Hosting, 12 months",
          "qty": 1,
          "price": 120
        }
      ]
    },
    {
      "number": "INV-1002",
      "date": "2026-10-08",
      "customer": "Sample Studio",
      "currency": "EUR",
      "items": [
        {
          "name": "Consulting hours",
          "qty": 6,
          "price": 80
        },
        {
          "name": "Travel",
          "qty": 1,
          "price": 95.5
        }
      ]
    },
    {
      "number": "INV-1003",
      "date": "2026-10-15",
      "customer": "Demo Supplies Co.",
      "currency": "SAR",
      "items": [
        {
          "name": "Printed brochures",
          "qty": 500,
          "price": 1.2
        },
        {
          "name": "Delivery",
          "qty": 1,
          "price": 60
        }
      ]
    }
  ],
  "fileName": "invoice-{{number}}",
  "pageFormat": "A4",
  "landscape": false,
  "zipAll": false,
  "mergeAll": false,
  "marginTop": "15mm",
  "marginRight": "12mm",
  "marginBottom": "15mm",
  "marginLeft": "12mm",
  "pageNumbers": true,
  "direction": "auto",
  "defaultFont": "noto-sans",
  "printBackground": true,
  "scale": 1,
  "preferCssPageSize": false,
  "taggedPdf": false,
  "allowedAssetHosts": [],
  "enableJavaScript": true,
  "waitMs": 0,
  "maxPdfs": 100
}
```

# Actor output Schema

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

No description

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

No description

## `bundles` (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 = {
    "template": `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Invoice {{number}}</title>
<style>
  body { font-size: 11pt; color: #102a43; }
  .top { display: flex; justify-content: space-between; align-items: flex-start; }
  h1 { margin: 0; font-size: 22pt; }
  .ar { font-family: 'Noto Sans Arabic', sans-serif; direction: rtl; }
  table { width: 100%; border-collapse: collapse; margin-top: 18px; }
  th, td { border-bottom: 1px solid #d9e2ec; padding: 8px; text-align: start; }
  th { background: #f0f4f8; }
  .num { text-align: end; }
  .total td { font-weight: bold; border-top: 2px solid #102a43; }
</style>
</head>
<body>
  <div class="top">
    <div>
      <h1>Invoice</h1>
      <div>No. {{number}}</div>
      <div>Date: {{formatDate date style="long"}}</div>
    </div>
    <div class="ar">
      <h1>فاتورة</h1>
      <div>رقم {{number}}</div>
      <div>التاريخ: {{formatDate date style="long" locale="ar-SA-u-ca-gregory"}}</div>
    </div>
  </div>
  <p><strong>Bill to:</strong> {{customer}}</p>
  <table>
    <thead><tr><th>Item</th><th class="num">Qty</th><th class="num">Unit price</th><th class="num">Amount</th></tr></thead>
    <tbody>
      {{#each items}}
      <tr><td>{{name}}</td><td class="num">{{qty}}</td><td class="num">{{formatCurrency price ../currency}}</td><td class="num">{{formatCurrency (multiply qty price) ../currency}}</td></tr>
      {{/each}}
      <tr class="total"><td colspan="3">Total / الإجمالي</td><td class="num">{{formatCurrency (sum items "qty" "price") currency}}</td></tr>
    </tbody>
  </table>
</body>
</html>`,
    "data": [
        {
            "number": "INV-1001",
            "date": "2026-10-01",
            "customer": "Example Trading LLC",
            "currency": "USD",
            "items": [
                {
                    "name": "Design work",
                    "qty": 10,
                    "price": 45
                },
                {
                    "name": "Hosting, 12 months",
                    "qty": 1,
                    "price": 120
                }
            ]
        },
        {
            "number": "INV-1002",
            "date": "2026-10-08",
            "customer": "Sample Studio",
            "currency": "EUR",
            "items": [
                {
                    "name": "Consulting hours",
                    "qty": 6,
                    "price": 80
                },
                {
                    "name": "Travel",
                    "qty": 1,
                    "price": 95.5
                }
            ]
        },
        {
            "number": "INV-1003",
            "date": "2026-10-15",
            "customer": "Demo Supplies Co.",
            "currency": "SAR",
            "items": [
                {
                    "name": "Printed brochures",
                    "qty": 500,
                    "price": 1.2
                },
                {
                    "name": "Delivery",
                    "qty": 1,
                    "price": 60
                }
            ]
        }
    ],
    "fileName": "invoice-{{number}}",
    "maxPdfs": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("maliksaid/html-to-pdf").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 = {
    "template": """<!doctype html>
<html>
<head>
<meta charset=\"utf-8\">
<title>Invoice {{number}}</title>
<style>
  body { font-size: 11pt; color: #102a43; }
  .top { display: flex; justify-content: space-between; align-items: flex-start; }
  h1 { margin: 0; font-size: 22pt; }
  .ar { font-family: 'Noto Sans Arabic', sans-serif; direction: rtl; }
  table { width: 100%; border-collapse: collapse; margin-top: 18px; }
  th, td { border-bottom: 1px solid #d9e2ec; padding: 8px; text-align: start; }
  th { background: #f0f4f8; }
  .num { text-align: end; }
  .total td { font-weight: bold; border-top: 2px solid #102a43; }
</style>
</head>
<body>
  <div class=\"top\">
    <div>
      <h1>Invoice</h1>
      <div>No. {{number}}</div>
      <div>Date: {{formatDate date style=\"long\"}}</div>
    </div>
    <div class=\"ar\">
      <h1>فاتورة</h1>
      <div>رقم {{number}}</div>
      <div>التاريخ: {{formatDate date style=\"long\" locale=\"ar-SA-u-ca-gregory\"}}</div>
    </div>
  </div>
  <p><strong>Bill to:</strong> {{customer}}</p>
  <table>
    <thead><tr><th>Item</th><th class=\"num\">Qty</th><th class=\"num\">Unit price</th><th class=\"num\">Amount</th></tr></thead>
    <tbody>
      {{#each items}}
      <tr><td>{{name}}</td><td class=\"num\">{{qty}}</td><td class=\"num\">{{formatCurrency price ../currency}}</td><td class=\"num\">{{formatCurrency (multiply qty price) ../currency}}</td></tr>
      {{/each}}
      <tr class=\"total\"><td colspan=\"3\">Total / الإجمالي</td><td class=\"num\">{{formatCurrency (sum items \"qty\" \"price\") currency}}</td></tr>
    </tbody>
  </table>
</body>
</html>""",
    "data": [
        {
            "number": "INV-1001",
            "date": "2026-10-01",
            "customer": "Example Trading LLC",
            "currency": "USD",
            "items": [
                {
                    "name": "Design work",
                    "qty": 10,
                    "price": 45,
                },
                {
                    "name": "Hosting, 12 months",
                    "qty": 1,
                    "price": 120,
                },
            ],
        },
        {
            "number": "INV-1002",
            "date": "2026-10-08",
            "customer": "Sample Studio",
            "currency": "EUR",
            "items": [
                {
                    "name": "Consulting hours",
                    "qty": 6,
                    "price": 80,
                },
                {
                    "name": "Travel",
                    "qty": 1,
                    "price": 95.5,
                },
            ],
        },
        {
            "number": "INV-1003",
            "date": "2026-10-15",
            "customer": "Demo Supplies Co.",
            "currency": "SAR",
            "items": [
                {
                    "name": "Printed brochures",
                    "qty": 500,
                    "price": 1.2,
                },
                {
                    "name": "Delivery",
                    "qty": 1,
                    "price": 60,
                },
            ],
        },
    ],
    "fileName": "invoice-{{number}}",
    "maxPdfs": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("maliksaid/html-to-pdf").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 '{
  "template": "<!doctype html>\\n<html>\\n<head>\\n<meta charset=\\"utf-8\\">\\n<title>Invoice {{number}}</title>\\n<style>\\n  body { font-size: 11pt; color: #102a43; }\\n  .top { display: flex; justify-content: space-between; align-items: flex-start; }\\n  h1 { margin: 0; font-size: 22pt; }\\n  .ar { font-family: '\''Noto Sans Arabic'\'', sans-serif; direction: rtl; }\\n  table { width: 100%; border-collapse: collapse; margin-top: 18px; }\\n  th, td { border-bottom: 1px solid #d9e2ec; padding: 8px; text-align: start; }\\n  th { background: #f0f4f8; }\\n  .num { text-align: end; }\\n  .total td { font-weight: bold; border-top: 2px solid #102a43; }\\n</style>\\n</head>\\n<body>\\n  <div class=\\"top\\">\\n    <div>\\n      <h1>Invoice</h1>\\n      <div>No. {{number}}</div>\\n      <div>Date: {{formatDate date style=\\"long\\"}}</div>\\n    </div>\\n    <div class=\\"ar\\">\\n      <h1>فاتورة</h1>\\n      <div>رقم {{number}}</div>\\n      <div>التاريخ: {{formatDate date style=\\"long\\" locale=\\"ar-SA-u-ca-gregory\\"}}</div>\\n    </div>\\n  </div>\\n  <p><strong>Bill to:</strong> {{customer}}</p>\\n  <table>\\n    <thead><tr><th>Item</th><th class=\\"num\\">Qty</th><th class=\\"num\\">Unit price</th><th class=\\"num\\">Amount</th></tr></thead>\\n    <tbody>\\n      {{#each items}}\\n      <tr><td>{{name}}</td><td class=\\"num\\">{{qty}}</td><td class=\\"num\\">{{formatCurrency price ../currency}}</td><td class=\\"num\\">{{formatCurrency (multiply qty price) ../currency}}</td></tr>\\n      {{/each}}\\n      <tr class=\\"total\\"><td colspan=\\"3\\">Total / الإجمالي</td><td class=\\"num\\">{{formatCurrency (sum items \\"qty\\" \\"price\\") currency}}</td></tr>\\n    </tbody>\\n  </table>\\n</body>\\n</html>",
  "data": [
    {
      "number": "INV-1001",
      "date": "2026-10-01",
      "customer": "Example Trading LLC",
      "currency": "USD",
      "items": [
        {
          "name": "Design work",
          "qty": 10,
          "price": 45
        },
        {
          "name": "Hosting, 12 months",
          "qty": 1,
          "price": 120
        }
      ]
    },
    {
      "number": "INV-1002",
      "date": "2026-10-08",
      "customer": "Sample Studio",
      "currency": "EUR",
      "items": [
        {
          "name": "Consulting hours",
          "qty": 6,
          "price": 80
        },
        {
          "name": "Travel",
          "qty": 1,
          "price": 95.5
        }
      ]
    },
    {
      "number": "INV-1003",
      "date": "2026-10-15",
      "customer": "Demo Supplies Co.",
      "currency": "SAR",
      "items": [
        {
          "name": "Printed brochures",
          "qty": 500,
          "price": 1.2
        },
        {
          "name": "Delivery",
          "qty": 1,
          "price": 60
        }
      ]
    }
  ],
  "fileName": "invoice-{{number}}",
  "maxPdfs": 100
}' |
apify call maliksaid/html-to-pdf --silent --output-dataset

```

## MCP server setup

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

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/5NEJ4H3Kjp0k6A6rW/builds/kAzBAwXxSErpsx5IJ/openapi.json
