HTML to PDF API

HTML to PDF API that renders exactly what your browser shows

Send HTML and CSS and get a print-ready PDF back: median render time 216 ms1. No headless Chrome to host, patch or scale.

  • 50 free renders/month
  • No credit card
  • Chromium 153

What is an HTML to PDF API?

An HTML to PDF API is a web service that turns HTML and CSS into a PDF file. You send your markup in an HTTP request; the service loads it in a headless browser, applies print styles, page size, margins, headers and footers, and returns the finished PDF, so you never have to run or scale a browser yourself.

Dynamic Document API does exactly that with one endpoint, POST /v1/pdf/from-html. The facts at a glance:

HTML to PDF API key facts
EndpointPOST https://api.dynamicdocumentapi.com/v1/pdf/from-html
InputHTML plus optional <head> content (CSS, fonts, scripts); optional JSON data for {{ variables }}
OutputPDF as a signed URL (default), raw binary or base64
Rendering engineEngine 2026.4 on Chromium (Chrome 153.0.8010.47), JavaScript on by default
Render timeMedian 216 ms, p95 516 ms, p99 667 ms (queue + processing, excluding network)1
Page sizes13 named sizes (A0–A6, B4, B5, Letter, Legal, Tabloid, Ledger) or any custom size from 0.1 to 200 inches
DeliverySynchronous by default; asynchronous with webhooks for long jobs
Data residencyPayloads and files stay in the EU
Client librariesPlain REST and JSON: any HTTP client works. Examples below for cURL, Node.js, Python and PHP
PriceFree: 50 renders/month. Paid from €15/month (billed annually) for 3,000 renders, auto top-ups from €6 per 1,000
Last updated

1. Measured over 1,664 successful renders on engine 2026.4. Render time is total_ms: time in the queue plus processing in the worker, excluding API overhead and network transfer. p75 378 ms, p90 478 ms, maximum 2,261 ms.

Convert HTML to PDF in one API request

Every request carries your HTML, optional <head> content, and a pdf object with the print settings. The example below renders a US Letter page with 20 mm top and bottom margins and a page-number footer. The cURL and Python versions ask for the file itself ("delivery": "binary"); the Node.js and PHP versions use the default and get back JSON with a signed download URL.

curl https://api.dynamicdocumentapi.com/v1/pdf/from-html \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<h1>Hello from HTML</h1><p>Rendered by Chromium.</p>",
    "head": "<style>body{font-family:system-ui} h1{color:#4751e9}</style>",
    "pdf": {
      "paper":  { "size": "Letter" },
      "margin": { "top": "20mm", "bottom": "20mm" },
      "footer": { "enabled": true, "center": "Page {{page}} of {{pages}}" }
    },
    "delivery": "binary"
  }' \
  --output hello.pdf

# hello.pdf is written to the current directory
// Node.js 18+ (built-in fetch)
const res = await fetch("https://api.dynamicdocumentapi.com/v1/pdf/from-html", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.DYNAMIC_DOCUMENT_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html: "<h1>Hello from HTML</h1><p>Rendered by Chromium.</p>",
    head: "<style>body{font-family:system-ui} h1{color:#4751e9}</style>",
    pdf: {
      paper:  { size: "Letter" },
      margin: { top: "20mm", bottom: "20mm" },
      footer: { enabled: true, center: "Page {{page}} of {{pages}}" },
    },
  }),
});

const render = await res.json();
console.log(render.files[0].url);
// signed URL, valid for 1 hour by default
import os
import requests

res = requests.post(
    "https://api.dynamicdocumentapi.com/v1/pdf/from-html",
    headers={"Authorization": f"Bearer {os.environ['DYNAMIC_DOCUMENT_API_KEY']}"},
    json={
        "html": "<h1>Hello from HTML</h1><p>Rendered by Chromium.</p>",
        "head": "<style>body{font-family:system-ui} h1{color:#4751e9}</style>",
        "pdf": {
            "paper":  {"size": "Letter"},
            "margin": {"top": "20mm", "bottom": "20mm"},
            "footer": {"enabled": True, "center": "Page {{page}} of {{pages}}"},
        },
        "delivery": "binary",
    },
    timeout=60,
)
res.raise_for_status()

with open("hello.pdf", "wb") as f:
    f.write(res.content)
<?php
$payload = [
    'html' => '<h1>Hello from HTML</h1><p>Rendered by Chromium.</p>',
    'head' => '<style>body{font-family:system-ui} h1{color:#4751e9}</style>',
    'pdf'  => [
        'paper'  => ['size' => 'Letter'],
        'margin' => ['top' => '20mm', 'bottom' => '20mm'],
        'footer' => ['enabled' => true, 'center' => 'Page {{page}} of {{pages}}'],
    ],
];

$ch = curl_init('https://api.dynamicdocumentapi.com/v1/pdf/from-html');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('DYNAMIC_DOCUMENT_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode($payload),
]);

$render = json_decode(curl_exec($ch), true);
echo $render['files'][0]['url'];

Without "delivery": "binary", a successful request returns 200 OK and a render object. Abridged:

{
  "id": "rnd_01J9ZM1X3F7R8K2C4V6B8N0P2Q",
  "status": "succeeded",
  "engine": "2026.4",
  "files": [{
    "format": "pdf",
    "pages": 1,
    "bytes": 18342,
    "url": "https://files.dynamicdocumentapi.com/f/…",
    "url_expires_at": "2026-09-22T15:06:34Z"
  }],
  "timings": { "total_ms": 214 }
}

Request parameters

The most-used fields. The full reference, including every enum value and limit, is in the API documentation.

Parameters of POST /v1/pdf/from-html
FieldWhat it does
htmlRequired. The markup to render. When data is set, {{ variables }} are filled in first.
headContents of <head>: <style>, <link>, <script>, <meta>.
dataJSON values for the variables in html and head.
pdf.paperA named size (A4, Letter, …) or width and height with units, e.g. "80mm".
pdf.orientationportrait or landscape.
pdf.margintop, right, bottom, left as CSS lengths ("20mm", "0.5in").
pdf.scaleZoom factor from 0.1 to 2.0.
pdf.print_backgroundPrint background colors and images. Default true.
pdf.emulate_mediaprint (default) or screen CSS media.
pdf.header, pdf.footerRunning header and footer, as HTML or as left/center/right text with {{page}} and {{pages}}.
pdf.waitWhen to capture: until = load, networkidle, selector, ready_flag or delay; timeout_ms up to 300,000.
pdf.locale, pdf.timezoneLocale and time zone the page runs in, e.g. en-US and America/New_York.
deliveryurl (default, signed link), binary or base64.
mode, webhooksync (default) or async, with a webhook that fires when the file is ready.

What the rendering engine supports

The engine is Chromium, the same code base as desktop Chrome, pinned to a tested release (engine 2026.4 runs Chrome 153). If your page prints correctly in Chrome, it renders the same way here. HTML to PDF is one input of our complete PDF generation API; the print features below are shared by all of them.

Modern CSS: flexbox, grid, @page and print media queries

Flexbox, grid, custom properties and @media print work as in current Chrome. Print media is emulated by default. Set prefer_css_page_size to let @page rules decide the page size.

@page { size: Letter; margin: 18mm 16mm; }
@media print { .no-print { display: none; } }

Headers, footers and automatic page numbers

Use simple left/center/right slots with the tokens {{page}}, {{pages}} and {{date}}, or a full HTML header with its own styles. Elements with the classes page, pages and title are filled in on every page.

"footer": { "enabled": true,
  "right": "Page {{page}} of {{pages}}" }

Custom fonts and web fonts

Load fonts the way you would on a website: a <link> to a font stylesheet or an @font-face rule with a remote URL, fetched through our proxy. Fonts from the engine's built-in catalog work without any setup.

Before capture, the engine waits for the page's load event, for fonts to finish loading, for images to decode and for two rendered frames. That removes the classic "PDF printed in the fallback font" bug.

JavaScript, charts and dynamic content

JavaScript runs by default, so Chart.js, D3, or a client-rendered app can draw before the PDF is taken. Tell the engine what "ready" means: networkidle (500 ms without requests), a CSS selector, a ready_flag your code sets, or a fixed delay.

"wait": { "until": "selector",
  "selector": "#chart[data-ready]",
  "timeout_ms": 15000, "strict": true }

With strict: true a timeout fails the render; with false you get the PDF plus a wait_timeout warning.

Page breaks and long tables

Control breaks with standard CSS: break-before: page starts a new page, break-inside: avoid keeps a row or a card together. A table's <thead> repeats at the top of every page it spans, so a 40-row invoice stays readable on page three.

.section   { break-before: page; }
tr, .card  { break-inside: avoid; }

Any page size

Pick one of 13 named sizes (A0–A6, B4, B5, Letter, Legal, Tabloid, Ledger) or set width and height freely from 0.1 to 200 inches. single_page makes one continuous page as tall as the content, which is how 58 mm and 80 mm thermal receipts are printed. page_ranges returns only the pages you need, for example "1-3,5".

Password protection, bookmarks and metadata

protect sets a user and an owner password plus seven individual permissions, such as printing or copying. outline builds bookmarks from your h1–h6, tagged adds the structure screen readers use, and metadata sets title, author, subject, keywords and language.

Locale, time zone and colors

Dates and numbers that your page's JavaScript formats come out right when you set locale and timezone; a US customer sees 9/22/2026, a German one 22.9.2026. Backgrounds and brand colors print by default, no "background graphics" checkbox required.

HTML to PDF API vs. self-hosted Puppeteer vs. wkhtmltopdf

Most teams that search for an HTML to PDF service already have one of two alternatives in mind: running headless Chrome themselves with Puppeteer or Playwright, or the wkhtmltopdf binary. Here is how they compare. We only put numbers where we can measure them; for self-hosting, the honest answer is "it depends on your setup".

HTML to PDF API compared with self-hosted Puppeteer and wkhtmltopdf
Criterion Dynamic Document API Self-hosted Puppeteer / Playwright wkhtmltopdf
SetupAPI key and one HTTPS requestInstall Chromium and fonts, build a render service with a queueInstall the binary and system fonts
EngineChromium (Chrome 153), updated by us with each engine releaseChromium, the version you pin and patchLegacy Qt WebKit; repository archived in 2023
CSS grid and flexboxYesYesNo grid; flexbox only in the old -webkit-box syntax
JavaScriptYes, with built-in wait conditionsYes, you write the wait logicOld JS engine; waits via a fixed delay or window.status
MaintenanceNone on your sideBrowser updates, security patches, memory leaks, stuck processesNo upstream security fixes anymore
ScalingConcurrency per plan; async jobs and webhooksYour browser pool, queue and autoscalingYour processes and servers
Render timep50 216 ms, p95 516 ms (queue + processing)Depends on cold starts and pool sizeDepends on your hardware
Cost at 10,000 PDFs/month€47/month (Growth, 15,000 renders, billed annually) or €59 monthlyServers plus engineering timeServers plus engineering time
Choose it whenYou want correct PDFs without running browsersData must never leave your network, or you already operate a browser fleetYou maintain a legacy system and cannot change it yet

When a hosted API is not the right choice

If your documents must be generated in an air-gapped network, or policy forbids sending their content to any third party, self-host Chromium. The same applies if you already run a well-tuned browser fleet at very high volume: at that point the API mainly saves you operations work, not money. For everyone else, the hosted route removes the part of PDF generation that breaks at 2 a.m.: the browser.

Moving off wkhtmltopdf

Because wkhtmltopdf is based on an old WebKit, layouts written for it often contain workarounds such as -webkit-box or float-based grids. They keep working in Chromium, so you can switch the renderer first and modernize the CSS later. Its page-size and margin flags map to pdf.paper and pdf.margin, and its header and footer HTML options map to pdf.header and pdf.footer.

Raw HTML or reusable templates?

Sending raw HTML is the right choice when your application already produces the markup, for example with React server rendering, Django, Rails or Laravel views. You keep the layout in your repository and the API only prints it.

When the same layout is filled with different data thousands of times, store it once and send only JSON. Reusable PDF templates use Jinja syntax, format dates and currencies per locale through ICU, render QR codes and barcodes, and keep every published version, so you can pin a version and roll back a layout change. A typical case is to turn an HTML invoice into a PDF for each order.

Writing long-form content such as reports, manuals or LLM output? You can also start from Markdown instead of HTML and pick one of four print themes.

Large documents: sync, async and webhooks

By default a request is synchronous: the connection stays open until the PDF is ready, and most renders finish in well under a second. If a render takes longer than your plan's sync limit, the API does not fail; it answers 202 Accepted with the render ID and keeps working. You fetch the result later from GET /v1/renders/{id}.

For large reports or batch jobs, send "mode": "async" with a webhook. The API answers immediately, and your endpoint receives render.succeeded or render.failed when the job is done. Send an Idempotency-Key header and you can retry a request safely: the same key returns the same render for 24 hours instead of creating a second one.

Synchronous and asynchronous HTML to PDF requests Synchronous: your app sends POST /v1/pdf/from-html and receives 200 OK with the PDF. Asynchronous: your app sends the request with mode async, receives 202 Accepted with a render ID, and later Dynamic Document API calls your webhook with render.succeeded and the file URL. SYNCHRONOUS (DEFAULT) Your app waits for the reply Dynamic Document API renders in Chromium POST /v1/pdf/from-html 200 OK · PDF or signed URL ASYNCHRONOUS (LARGE JOBS) Your app continues working Dynamic Document API queues and renders POST … "mode": "async" 202 Accepted · render ID webhook: render.succeeded + URL
Synchronous requests return the PDF directly. Asynchronous requests return at once and notify your webhook when the file is ready.

Security and data handling

  • Regional processing. Render payloads and generated files stay in the region of the API host you call.
  • Signed, expiring links. Download URLs are signed and expire after one hour by default. Set expires_in anywhere from 60 seconds to 7 days, or skip hosting and take the file as binary.
  • Retention you control. Choose how long files are kept per request with retention_days, up to your plan's maximum, and purge them at any time with DELETE /v1/renders/{id}/files.
  • Your own storage. On Enterprise plans, files can go straight to your own S3-compatible bucket.
  • Safe asset loading. Images, fonts and stylesheets referenced by your HTML are fetched through a proxy that blocks private and internal network addresses.
  • Test and live keys. Separate API keys for development and production, and a Data Processing Agreement on every paid plan.

HTML to PDF API pricing

Start with 50 free renders a month, no credit card. Paid plans are flat monthly prices with a fixed volume and automatic top-ups, so the cost per document is easy to calculate and a pipeline never stops at month end.

Free

€0

50 renders per month, full REST API. For testing and prototypes.

Starter

€15 / month

3,000 renders per month, billed annually (€19 billed monthly). About €0.005 per render.

Higher volumes, team features and your own storage are on the larger plans. See the full HTML to PDF API pricing for every tier.

FAQ

HTML to PDF API: frequently asked questions

Does the API support CSS grid and flexbox?

Yes. The engine is Chromium (Chrome 153), so flexbox, grid, custom properties, @page rules and print media queries behave as they do in current Chrome. A quick way to preview a layout is Chrome's own print preview: it uses the same layout engine.

How do I add page numbers and a running footer?

Set pdf.footer to {"enabled": true, "center": "Page {{page}} of {{pages}}"}, or pass your own footer HTML with elements that carry the classes page and pages. Reserve room for it with margin.bottom. Headers work the same way through pdf.header.

Does it run JavaScript before generating the PDF?

Yes, JavaScript is on by default. Use pdf.wait.until to decide when the page is ready: networkidle, a CSS selector, a ready_flag your script sets, or a fixed delay. Set pdf.javascript to false to turn scripts off.

How long can a render take?

Most renders are fast: the median is 216 ms and p99 is 667 ms, measured as queue plus processing time without network. When a page waits for content, the wait timeout defaults to 30 seconds and can be raised to 300 seconds. If a synchronous request outlasts your plan's sync limit, the API answers 202 and finishes the render asynchronously.

Can I convert a URL instead of raw HTML?

Yes. POST /v1/pdf/from-url lets you convert a live URL to PDF, including pages behind a login via cookies, headers or basic auth. Credentials are only sent to the same domain. Custom CSS cannot be injected into a URL capture; if you need to restyle the page, send its HTML instead.

How is this different from wkhtmltopdf?

wkhtmltopdf renders with a legacy Qt WebKit engine, and its repository was archived in 2023, so it no longer receives fixes. It does not support CSS grid and handles modern JavaScript poorly. Dynamic Document API renders with current Chromium and you do not install or patch anything.

Is there a free HTML to PDF API tier?

Yes. The free plan includes 50 renders per month with the full REST API and no credit card. Paid plans start at €15 per month for 3,000 renders when billed annually, or €19 billed monthly.

Convert your first HTML to PDF free

50 renders a month on the free plan, no credit card, auto top-ups on paid plans, no browser to maintain.

Start free