HTML to PDF API — the complete 2026 guide cover illustration

HTML to PDF API — the complete 2026 guide

If you’re shopping for an HTML to PDF API in 2026, the market has quietly converged on a single architecture. Almost every serious vendor runs the same headless browser (Chromium), speaks the same basic request shape (POST some HTML, get back a PDF), and argues over a handful of feature knobs — margins, headers, encryption, webhooks, regions. The differences that actually matter to a working engineer are smaller than the marketing suggests, and they’re hidden under layers of feature-list bingo.

This guide cuts through that. It’s the piece I wish I had when we were designing 21pdf — written by the team that built it, grounded in the Go codebase that powers it, and honest about what an HTML to PDF API can and cannot do.

TL;DR

  • An HTML to PDF API is a REST service that renders HTML to a PDF, usually via headless Chromium.
  • The only rendering engines worth paying attention to in 2026 are Chromium (the one) and Prince XML (niche, print-shop grade, expensive). Everything else is dead or dying.
  • The six features that actually matter: CSS @page support, network-idle waiting, SSRF handling, quota + concurrency gates, async job model, and predictable pricing.
  • Managed APIs make sense until your volume crosses ~100k PDFs/day. Self-hosting Chromium at scale is a real operational commitment.
  • You rarely need webhooks, signed URLs, or S3 direct-upload on day one. Start with synchronous POST /convert → poll → download, then add complexity when you have the scars to justify it.

What an HTML to PDF API actually does

At its core, a modern HTML to PDF API is a thin wrapper around a headless browser. You send it HTML (or a URL, or a fragment), it launches a Chromium process, loads the page, waits for it to settle, and calls page.pdf() or the equivalent DevTools Protocol command. The PDF bytes come out the other end.

The interesting parts are everything around that core render loop:

  1. Accepting input safely — raw HTML, a URL to fetch, or a fragment. URL inputs need SSRF defences. Raw HTML can still reference external assets that need the same defences.
  2. Managing Chromium lifecycle — cold starts, browser crashes, zombie tabs, out-of-memory kills. You don’t want a browser per request; you don’t want one browser forever either.
  3. Handling back-pressure — quotas per customer, concurrent-render limits, graceful degradation when the queue deepens.
  4. Turning the raw PDF bytes into something deliverable — synchronous response, signed URL, storage upload, webhook callback.

A vendor’s real differentiation lives in those four concerns. The actual rendering — the call that produces PDF bytes from HTML — is essentially commodity. Everyone is using Chromium; the differences are microseconds on a warm process.

The three input shapes every API offers

Virtually every HTML-to-PDF API exposes three input modes:

InputUse whenCaveat
html (full document)Your service already has the rendered HTML in memoryLarge payloads eat bandwidth; 1-5MB is normal, 50MB is a smell
urlThe page you want to print is already published somewhereSSRF risk; slower by a round-trip
simple_html / fragmentYou have just a chunk and don’t want to write boilerplateAPI wraps it in a minimal document; sometimes strips things

21pdf accepts all three under a single POST /v1/convert endpoint. Most competitors do too; they just call the fields different names. Some APIs push you toward one shape (usually url) because it’s easier for them to cache — be wary. You want to send whichever shape is fastest for your system, which is usually raw HTML.

The engines that matter in 2026

The HTML-to-PDF rendering engine landscape has been consolidating for five years. Here’s the state of play.

Chromium (effectively: all of them)

Puppeteer and Playwright are the two common automation layers; underneath, both drive Chromium via the DevTools Protocol. API vendors generally use one or the other, or talk CDP directly.

Chromium wins because:

  • It supports every modern CSS feature: flex, grid, container queries, @page, subgrid, variable fonts, print-colour-adjust, and so on.
  • It’s the same engine users browse with, so WYSIWYG is a real phrase rather than marketing.
  • The print output is excellent — page counters, running headers, CSS hyphenation all work.
  • It’s free and patched aggressively (important because rendering untrusted HTML is a security-sensitive operation).

If you’re evaluating an HTML-to-PDF API and the vendor can’t tell you what Chromium version they run, that’s a bad sign. At 21pdf we pinned our base image after Alpine’s Chromium 131 package started segfaulting on certain fonts — the version matters.

Prince XML (niche, worth knowing)

Prince is the oldest actively-maintained HTML-to-PDF engine and the only serious alternative. It implements CSS Paged Media Level 3 more completely than Chromium does, supports advanced typography (proper ligatures, small caps, drop caps with @page flow), and produces tighter PDF output.

It’s also expensive ($3,800+ for a server licence) and single-threaded, so you need one licence per renderer. Book publishers, legal typesetters, and airline-ticket printers use it. Almost no SaaS uses it directly.

DocRaptor is the largest API in front of Prince. If your output must be print-shop quality and pixel-identical to a designer’s mockup, they’re worth the premium. If you’re rendering invoices and statements, Prince is a sledgehammer for a finishing nail.

wkhtmltopdf (dead; don’t start here)

wkhtmltopdf was the dominant open-source tool until 2020. It’s based on a 2013-era WebKit snapshot that upstream abandoned — no flex, no grid, no modern @page. The project entered deprecation in 2023 and is effectively unmaintained.

We removed wkhtmltopdf from 21pdf in April 2026. Every conversion now goes through Chromium. If a vendor still prominently features “wkhtmltopdf compatibility” in their marketing, treat that as a code-smell — they have legacy users they don’t want to break, which means their product is also legacy.

Weasyprint, Gotenberg, others

  • Weasyprint is a Python-based renderer with decent @page support but significantly lagging CSS (no container queries, no subgrid, limited JS). Fine for simple reports; wrong for anything complex.
  • Gotenberg is a Dockerised Chromium wrapper — basically the self-hosted version of the APIs you’re evaluating. Good project; you end up operating it yourself.
  • LaTeX pipelines still exist in academic and publishing contexts but aren’t an HTML-to-PDF answer.

The only conversation that’s realistic for most teams in 2026 is Chromium-based APIs vs self-hosted Chromium. Everything else is a special case.

The six features that actually matter

Marketing pages will list thirty features. Most don’t move the decision. Here are the six that do.

1. CSS @page and paged-media support

The @page rule and its relatives (@page :first, @page :left, size, margin, page-break-before, break-inside) are how you control PDF layout from CSS. Without them you’re stuck with API-level knobs (margin_top: 20, orientation: portrait) that won’t express anything complex.

A good API lets your @page rules override its API-level knobs — i.e., if you specify size: A3 landscape in CSS, that wins over options.page_size: "A4" passed in the request. 21pdf does this because Chromium does this; you should confirm with any vendor you evaluate.

2. Network-idle waiting

Modern pages fetch data after first paint. If your renderer captures the PDF the moment DOMContentLoaded fires, you get a blank skeleton. The fix is a wait_for_network_idle option (sometimes called network_idle, wait_until: 'networkidle0', or idle_ms) that holds the render until XHRs settle.

Some APIs let you pin the wait to a CSS selector (wait_for: '.report-ready'), which is strictly better — “the element I actually need appeared” is a more reliable signal than “the network was quiet for 500ms”. Confirm which mechanism your candidate API supports.

3. SSRF handling

If your API accepts url inputs, it’s a server-side request forgery vector. A malicious customer can ask it to render http://169.254.169.254/latest/meta-data/iam/security-credentials/ (AWS metadata) or http://localhost:5432 (your vendor’s internal Postgres) and the PDF response bytes will leak whatever’s there.

The real defence is two layers:

  1. HTTP boundary block: reject any URL that resolves to 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, fe80::/10, or link-local before issuing the fetch.
  2. Chromium request interceptor: inside the browser, install a request handler on the page that re-checks every sub-request the page makes. A page can resolve a DNS name that currently points to a public IP but, by the time Chromium fetches it, has been reassigned to a private one (DNS rebinding). Only a re-check inside the browser catches this.

At 21pdf we do both. Many competitors do only the first. You can confirm by asking — if the answer is “we block private IPs at the HTTP layer” full stop, they haven’t thought hard enough.

Worth knowing A 2024 survey of 12 popular HTML-to-PDF APIs found 7 were vulnerable to DNS-rebinding SSRF at the Chromium layer even when they filtered the top-level URL. If SSRF matters to you, ask the vendor for their specific mitigation.

4. Quota and concurrency gates

An HTML-to-PDF API needs two separate rate limits:

  • Quota — total PDFs per billing cycle. Used for plan tiering.
  • Concurrency — simultaneous in-flight renders per customer. Used to prevent noisy neighbours.

These are different knobs. Without a concurrency gate, a single customer can submit 10,000 parallel jobs and push every other customer’s queue time through the roof. A good API will expose both limits on every error response (X-Quota-Used: 412, X-Quota-Limit: 10000, Retry-After: 5) so your client can self-rate-limit.

At 21pdf the quota gate lives in middleware in front of POST /v1/convert and returns 402 Payment Required when exhausted; the concurrency gate returns 429 with Retry-After. Two different status codes for two different classes of back-pressure; don’t conflate them.

5. Async job model (don’t block your API on our rendering)

A Chromium render takes 200-3,000ms depending on the page. If your API call is a synchronous request/response, you’re tying up an HTTP connection for that whole window — both on your side and on the vendor’s. At scale this becomes a problem for both of you.

The robust shape is async:

POST /v1/convert → 202 Accepted + { job_id }
GET  /v1/jobs/:id → { status, pdf_url? }  (poll or SSE)
GET  /v1/jobs/:id/download → PDF binary

This is what 21pdf does by default. Some APIs offer sync as an option (block for up to 30 seconds, get the PDF back in the response). That’s fine for dev work; for production, async is safer — your HTTP server isn’t holding 10,000 open connections waiting for Chromium processes.

6. Predictable, inspectable pricing

You want an API whose pricing page is a table, not a calculator. Pay-per-PDF sounds flexible; in practice it makes cost prediction harder and opens the door to surprise bills during traffic spikes.

Flat subscription tiers with a clear over-quota behaviour (hard 402 at 21pdf; soft throttle elsewhere) are easier to reason about. Inspect the over-quota path specifically — if you can’t tell what happens at your quota’s 10,001st PDF, that’s a contract you can’t plan around.

Shape of a real production integration

Here’s the minimum viable integration with an HTML-to-PDF API. It’s what I’d build first, and it’s what works for 80% of production workloads.

Client side

# Single curl to submit a render.
curl -X POST https://login.21pdf.com/v1/convert \
  -H "Authorization: Bearer $QPDF_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<h1>Invoice #2041</h1>...",
    "options": {
      "page_size": "A4",
      "margin_top": 20, "margin_bottom": 20,
      "margin_left": 15, "margin_right": 15,
      "wait_for_network_idle": true
    }
  }'

Response:

{ "job_id": "job_01HW...", "status": "queued" }

Then poll until status is succeeded and download. In Node:

async function renderPdf(html) {
  const sub = await fetch('https://login.21pdf.com/v1/convert', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.QPDF_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      html,
      options: { page_size: 'A4', wait_for_network_idle: true },
    }),
  }).then(r => r.json());

  // Poll every 500ms, capped at 30s.
  for (let i = 0; i < 60; i++) {
    await new Promise(r => setTimeout(r, 500));
    const s = await fetch(`https://login.21pdf.com/v1/jobs/${sub.job_id}`, {
      headers: { 'Authorization': `Bearer ${process.env.QPDF_KEY}` },
    }).then(r => r.json());
    if (s.status === 'succeeded') {
      return fetch(`https://login.21pdf.com/v1/jobs/${sub.job_id}/download`, {
        headers: { 'Authorization': `Bearer ${process.env.QPDF_KEY}` },
      }).then(r => r.arrayBuffer());
    }
    if (s.status === 'failed') throw new Error(s.message);
  }
  throw new Error('timeout');
}

That’s it. 40 lines, no SDK, works against every HTTP-clean API with cosmetic adjustments.

In Python it’s even shorter:

import os, time, requests

def render_pdf(html: str) -> bytes:
    key = os.environ["QPDF_KEY"]
    headers = {"Authorization": f"Bearer {key}"}

    sub = requests.post(
        "https://login.21pdf.com/v1/convert",
        headers={**headers, "Content-Type": "application/json"},
        json={"html": html, "options": {"page_size": "A4", "wait_for_network_idle": True}},
    ).json()

    for _ in range(60):
        time.sleep(0.5)
        s = requests.get(f"https://login.21pdf.com/v1/jobs/{sub['job_id']}", headers=headers).json()
        if s["status"] == "succeeded":
            return requests.get(
                f"https://login.21pdf.com/v1/jobs/{sub['job_id']}/download",
                headers=headers,
            ).content
        if s["status"] == "failed":
            raise RuntimeError(s["message"])
    raise TimeoutError()

Server side (what the vendor is doing)

When your request hits the API, roughly this happens:

  1. Auth middleware resolves Authorization: Bearer ... to a user id.
  2. Billing middleware checks the user’s quota and concurrency gates. If either is exceeded, return 402 or 429 without touching the render path.
  3. A job row is inserted into Postgres with status = queued.
  4. The response returns the job_id; the HTTP connection closes.
  5. A worker pool picks the job up, launches a Chromium page, sends the HTML, waits for network idle (if requested), calls page.pdf(), uploads the bytes to object storage.
  6. The job row is updated to status = succeeded.
  7. Your polling hits /v1/jobs/:id, sees succeeded, follows with /download.
  8. The download endpoint streams the bytes from object storage back to you.

Nothing exotic in there. The engineering that matters is in the details — how the worker pool handles crashed Chromium processes, how the object-storage path is SSE-encrypted, how quotas are race-safe under concurrent increments. At 21pdf we’ve written these components over time; competitors have written their own variations; the differences are real but granular.

Common pitfalls (and how to avoid them)

“My PDF is blank / missing data”

The request rendered faster than your data arrived. Fix: add wait_for_network_idle: true or pin to a selector (wait_for: '.ready'). If you control the page, add a window.__reportReady = true setter and have the API wait on it — most APIs accept a JS expression as a wait condition.

”My charts render as empty divs”

Most chart libraries paint to a canvas after DOM ready. You need network-idle plus a small additional delay (delay_ms: 300) to catch the canvas draw. Alternatively, many chart libraries have a server-side render mode — use that if possible.

”My fonts look wrong”

Chromium’s font fallback kicks in when your @font-face declaration points at a CDN that hasn’t loaded yet, or your font-family name doesn’t match. Check that:

  • @font-face src URLs are reachable from the vendor’s infra (they usually are; corporate intranets break this)
  • network-idle is enabled (so the font fetch completes)
  • you’re declaring exact font-family names, not category fallbacks

”Page breaks are in the wrong place”

You probably have a display: flex container that doesn’t break across pages. Chromium respects break-inside: avoid, break-before: page, break-after: page, and the legacy page-break-* family. Walk your HTML and insert breaks explicitly at the right boundaries.

”Quota errors at inopportune moments”

Check whether your API’s quota reset is calendar-aligned (the 1st of the month) or cycle-aligned (the day you subscribed). 21pdf resets on the subscription.charged webhook from our billing provider, so it’s cycle-aligned; some competitors are calendar-aligned. If you bill your customers monthly on the 1st and your vendor resets on the 15th, you’ll have a perpetually-confusing quota-accounting discrepancy.

”Concurrency limit hits during batch jobs”

Your plan’s concurrency limit was designed for interactive traffic. Batch jobs need to self-throttle — send N requests where N ≤ concurrency, wait for one to complete before sending the next. This is a client-side job queue; most teams write it in 50 lines and move on.

Self-host vs managed

Every evaluation eventually asks this question. The honest answer is volume-dependent:

Daily PDFsRecommendation
< 1,000Managed API. Pay the $20-$100/month and spend zero hours on infrastructure.
1,000 - 10,000Managed API. The economics still favour it; you have bigger fish.
10,000 - 100,000Flip a coin. If PDF rendering is central to your product and you have spare Kubernetes capacity, self-hosting is viable. Otherwise stay managed.
> 100,000 / daySelf-host, or negotiate a volume deal with a managed vendor. At this scale the 50-80% margin vendors take starts to matter.

Self-hosting isn’t just “run Chromium in a container” — it’s:

  • Keeping the Chromium binary patched (weekly-to-monthly for security CVEs)
  • Managing the font catalogue (you’d be surprised how often missing fonts cause tickets)
  • Running a worker pool with health checks, OOM handling, and process recycling (Chromium leaks memory over thousands of tabs)
  • Implementing the same SSRF, quota, and concurrency gates you’d otherwise buy
  • Operating object storage for output PDFs
  • Monitoring render-time distributions and alerting on degradation
  • Handling enforcement — what happens when a customer submits 10,000 parallel jobs

Each is tractable; the cumulative operational cost is a full-time engineer’s partial attention. If rendering PDFs is not your product, outsource it.

A quick note on SSRF

I mentioned SSRF above; it deserves a longer note because it’s the security issue most vendors get wrong.

Any API that accepts a url input is effectively letting customers make HTTP requests from inside your infrastructure. Without defences, they can:

  • Read AWS/GCP metadata endpoints (169.254.169.254) and exfiltrate IAM credentials
  • Probe internal services (your Postgres on localhost:5432, your Redis, your admin panels)
  • Pivot to internal subnets that should be unreachable from the internet

The minimum viable defence is the two-layer check: block private IP ranges at the HTTP boundary, re-check inside Chromium. The harder cases are:

  • DNS rebinding: Attacker-controlled DNS returns a public IP at boundary-check time, then a private IP when Chromium actually fetches. Defence: re-check inside the browser on every request.
  • HTTP redirects: You validate https://attacker.com/redirect-me, Chromium follows it to http://169.254.169.254/. Defence: validate the redirect target too, or disable auto-redirects and walk them yourself.
  • WebSocket / EventSource: Browser sub-requests don’t all go through fetch. Your request interceptor has to cover WS, SSE, and dynamic imports.

The CVE archive is full of HTML-to-PDF APIs that got one of these wrong. If you care about SSRF (and you should, if you handle enterprise customers), ask the vendor for their specific mitigation strategy before you sign.

Pricing, briefly

Every vendor prices differently, but the market has settled into a narrow range for 10k PDFs/month:

Vendor (approx)10k PDFs/monthModel
21pdf$29Flat subscription
PDFShift$29Flat subscription
DocRaptor$99Flat (Prince engine — premium)
API2PDF~$35Pay-per-PDF ~$0.004
PDFCrowd$29Flat subscription
PDFMonkey$29Flat subscription

All within 2-4× of each other. For most teams the decision isn’t price; it’s whether the specific features you need (SSRF hardening, @page support, network-idle, SLA) are ones you trust the vendor to have gotten right.

21pdf’s pricing: $0 Free (100 PDFs/mo) · $9 Starter (1,000) · $29 Pro (10,000) · $69 Business (50,000). Every paid plan includes every API feature; plans differ only on monthly throughput and concurrency. Pricing page has the full comparison.

Build on 21pdf

A precision HTML-to-PDF API with Chromium rendering, SSRF two-layer defence, async job model, and per-plan quotas. Free tier: 100 PDFs/month. No card required.

Get an API key → Read the docs

What to ask a vendor before you pick one

A checklist I’d actually use, in rough priority order:

  1. What Chromium version do you run, and how often do you patch it? “We’re on Chromium 128, patched within 7 days of upstream” is a good answer. Silence is a bad answer.
  2. How do you handle SSRF for URL inputs? Listen for “two layers” or “DNS rebinding” in the response. If you hear only “we filter private IPs”, that’s incomplete.
  3. Do you support CSS @page / paged-media? Yes is table stakes.
  4. Can I wait for network idle / a specific selector / a JS expression? Network idle is table stakes; selector/JS is a plus.
  5. What’s your async model — sync-only, async-only, or both? Both is nice; async-only (what 21pdf does) is fine for production but slightly noisier for dev.
  6. What are the quota and concurrency limits, and what error codes do they return? Distinct 402 vs 429 is the correct answer.
  7. What happens at the 10,001st PDF on a 10k plan? Hard block, soft throttle, or overage billing. All are viable; you just need to know.
  8. Is billing cycle-aligned or calendar-aligned? Not a dealbreaker, just needs to match your accounting.
  9. What’s your uptime over the last 12 months? Ask for a link to a public status page, not a marketing number.
  10. Can I self-serve an API key right now, or do I need a sales call? Self-serve is better. Every vendor on 21pdf’s list above is self-serve.

If you get clean answers to all 10, the vendor has thought about the product. If you get marketing deflection on half of them, move on.

Closing

An HTML-to-PDF API is, in 2026, a fairly commodity thing. Any vendor with a recent Chromium, sane SSRF handling, and a reasonable async model will render your invoices correctly. The differences that matter for your specific use case are narrow — and after five years of working on document infrastructure, I’m convinced that most teams overthink this decision.

Pick a vendor whose pricing page is a table, whose docs work, and whose error codes distinguish quota from concurrency. Integrate it in an afternoon. Come back in a month and tighten the parts that hurt — usually wait conditions and font fallbacks. Don’t switch until you have a concrete reason.

If you want to try 21pdf, the free tier is 100 PDFs/month with no card. It’ll take you about 10 minutes to get your first PDF rendered.

— 21pdf Engineering

Frequently asked questions

What is an HTML to PDF API?

A REST API that accepts HTML (or a URL, or a fragment) and returns a PDF. Almost every modern one runs a headless browser server-side — Chromium in most cases — because real CSS support is table stakes for anything more involved than a receipt.

Should I self-host or use a managed HTML to PDF API?

Self-host if PDF rendering is core to your product, you have at least one engineer on the team who wants to own a browser farm, and your volume justifies it (roughly >100k PDFs/day). Use a managed API otherwise — the operational cost of keeping Chromium patched, fonts provisioned, and rendering concurrent at scale is much larger than the list price of a good API.

Can an HTML to PDF API handle JavaScript-heavy pages?

Yes — any Chromium-based API executes JS before capturing the PDF. Look for a `wait_for_network_idle` or similar option; single-page apps that fetch data on mount will render blank without it.

What about headers, footers, and page numbers?

CSS `@page` margins and counters cover most cases in modern Chromium. APIs that expose a separate `header` / `footer` HTML object (with `&#123;pageNumber&#125;` tokens) give you per-page runtime injection — useful but not universally supported. 21pdf treats this as roadmap today; CSS `@page` works now.

How do HTML to PDF APIs prevent server-side request forgery (SSRF)?

Most good ones do it in two layers: block private IP ranges at the HTTP boundary before the fetch, then re-check every sub-request Chromium makes while rendering. Skipping the second layer is a common mistake — a page can `fetch('http://169.254.169.254/')` from inside the browser and exfiltrate cloud metadata.

How much should an HTML to PDF API cost?

Free tiers on managed APIs cluster around 50-100 PDFs/month. Paid tiers at 1k-10k PDFs land in the $9-$99/month range. Pay-per-PDF makes sense below 500/month; beyond that, flat subscriptions almost always win.

Is there a free HTML to PDF API?

Yes — 21pdf's Free tier is 100 PDFs/month with the full feature set. PDFShift, DocRaptor, and API2PDF all offer small free allotments too. Free tiers exist to help you ship an integration, not to run production.