Best HTML to PDF APIs (2026): 7 Options Compared
DocRaptor, PDFShift, Api2Pdf, PDFMonkey, Gotenberg, Browserless and RenderScreenshot compared: engines, prices at three volumes, code and an engine test.
Disclosure: I build RenderScreenshot, one of the seven services below. To keep that from tilting the page, every price comes from the vendor's own page with the date we read it, every claim about a competitor links to its docs, and the engine test includes the cases where our engine (Chrome) loses.
Method in brief. We wrote three test templates (an invoice, a modern-CSS layout page and a paged report) plus a paged-media stress file, and printed them on 2026-10-18 with the engines these services run: Chrome 148 through Puppeteer, Chrome 156 inside a local Browserless container, Prince 17 (the engine family behind DocRaptor), and wkhtmltopdf as the legacy baseline. We did not send the templates through the hosted services, because we hold no paid accounts with them. So this page compares engines, request shapes and list prices, and it labels every snippet as either run or not run.
The seven at a glance
| Service | Engine | How you send content | What comes back | Async | Free tier (Oct 2026) | Runs where |
|---|---|---|---|---|---|---|
| DocRaptor | Prince (pipeline 10.1 = Prince 15.1 by default) | HTML or URL in JSON | PDF bytes, or a hosted link (paid add-on) | Yes, up to 600 s | 5 docs a month + unlimited watermarked tests | Hosted |
| PDFShift | Chromium 116 or 142 | HTML or URL in JSON | PDF bytes, or JSON with a 2-day S3 link | Webhook | 50 credits a month + watermarked sandbox | Hosted |
| Api2Pdf | Chrome, plus wkhtmltopdf and LibreOffice endpoints | HTML or URL in JSON | JSON with a file link (deleted after 24 h) | Not checked | One month of credit for new accounts | Hosted |
| PDFMonkey | Chrome 133 ("v5" engine) | A stored Liquid template + JSON data | JSON with a signed link (valid 1 h) | Async by default, sync endpoint | 20 docs a month | Hosted |
| Gotenberg | Chromium (+ LibreOffice) | Multipart form with index.html |
PDF bytes | Webhook | Free, MIT licence | Your container |
| Browserless | Chrome | HTML or URL in JSON, or your own Puppeteer code | PDF bytes | No (you run the session) | 1,000 units a month (cloud) | Their cloud or your container |
| RenderScreenshot | Chrome on Cloudflare | HTML, URL or Markdown in JSON | PDF bytes + public CDN link | Webhook, 202 |
See pricing | Hosted |
Sources: each vendor's docs and pricing page, linked in its section below.
What the engine test showed
The hosted services wrap a small number of engines, so most fidelity questions are engine questions. Here is what each engine did with the same files.
Method. Templates from our corpus: invoice.html (60 rows over A4 pages, repeating table header, CSS custom properties), layout.html (nine modern CSS features, each with a text marker), report.html (cover plus five chapters, break-before: page, CSS page counters in an @page margin box). One cold and five warm runs per file. Chrome 148: Puppeteer 24.43.1 on macOS, Apple M1 Pro, waitUntil: 'load'. Chrome 156: Browserless 2.57.0 in Docker on the same machine, /pdf route. Prince 17: the prince-pdf Python package, free edition (adds a logo). wkhtmltopdf 0.12.6.1 with patched Qt: official arm64 build in a Debian 12 container. Text checked with pdftotext, layout checked by eye on rasterized pages.
| Engine | Invoice pages / bytes | Report pages | CSS page counters printed | Modern CSS features applied (of 9) | Warm time, median |
|---|---|---|---|---|---|
| Chrome 148 (Puppeteer) | 3 / 111,438 | 6 | 5 of 6 (cover suppressed, correct) | 9 | 245 to 371 ms |
| Chrome 156 (Browserless) | 3 / 105,863 | 11 | 10 of 11 | not rasterized | 222 to 278 ms |
| Prince 17 | 3 / 28,385 | 6 | 5 of 6 | 6 (no aspect-ratio, object-fit or color-mix()) |
140 to 240 ms (invoice only, CLI, including process start) |
| wkhtmltopdf 0.12.6.1 | 2 / 25,318 | 3 | 0 | 0 | about 270 ms |
What stands out:
- Chrome-based engines agree on layout, but not on page count. The Browserless report ran to 11 pages because the container had a different serif font than the Mac. That is a font problem, not a Chrome problem, and it will happen on any hosted Chrome service whose fonts differ from your laptop's. Load your own web fonts.
- Prince makes much smaller files. Its invoice was about a quarter of Chrome's size. If PDFs go out as email attachments at volume, that is a real cost difference.
- wkhtmltopdf failed most of the test: the grid stacked into one column,
break-before: pagewas ignored, and with custom properties unsupported our white-on-teal boxes printed white on white. Details are in our wkhtmltopdf alternative write-up. - Waiting costs time. In a second Puppeteer run with
waitUntil: 'networkidle0'instead ofload, the warm median rose to between 721 and 1,175 ms per document. Every hosted service has an equivalent switch. It is the right default for pages that fetch data or fonts, and the main reason hosted PDF times are often near one second.
Print features where Prince still wins
We also printed a paged-media stress file with Prince 17 and Chrome 148:
| Feature | Prince 17 | Chrome 148 |
|---|---|---|
@page margin boxes with counter(page) and counter(pages) |
Yes | Yes |
@page :first and named pages (page: wide with a landscape @page) |
Yes | Yes |
Running header from string-set |
Yes | No |
Table of contents numbers with target-counter() and leader() |
Yes | No |
Footnotes with float: footnote |
Yes | No, printed inline |
| PDF bookmarks from headings | Yes, by default | No |
| Tagged PDF | When asked (for example a PDF/UA profile) | Yes, by default |
If your documents need the middle three rows, only DocRaptor in this list will print them. The DocRaptor alternative page has the full test and workarounds for Chrome.
Best pick by job
| Job | Pick | Why |
|---|---|---|
| Books, manuals, contracts with cross-references | DocRaptor | Prince's paged-media support |
| Invoices and receipts from your own web app | RenderScreenshot, PDFShift or Api2Pdf | Chrome prints what your app shows |
| Lowest bill for many small documents | Api2Pdf | Pay per MB and per second |
| Non-developers edit the template | PDFMonkey | Templates and data live apart, with a template editor |
| Self-hosted, no per-document fee | Gotenberg | One container, MIT licence |
| Pages that need login, clicks or custom scripts first | Browserless | Run your own Puppeteer code |
| PDFs plus screenshots and social images from one key | RenderScreenshot | Same endpoint, output.format switches |
Prices at three volumes
Monthly list prices, read on each vendor's pricing page on 2026-10-18 unless noted. Each cell is the cheapest monthly option that covers the volume, assuming small documents (under 1 MB, about 2 seconds of render time).
| Service | 1,000 PDFs | 10,000 PDFs | 100,000 PDFs |
|---|---|---|---|
| DocRaptor | $75 (Premium, 1,250 docs) | $399 (Bronze, 15,000 docs) | Enterprise, contact sales |
| PDFShift | $24 (Boost, 2,500 credits) | $99 (Business, 25,000 credits) | $249 (Shift 100k) |
| Api2Pdf (0.2 MB, 2 s each) | about $1.59 | about $6.91 | about $60.10 |
| PDFMonkey | €15 (Pro, 3,000 docs) | about €64 (Pro + 7,000 pay-as-you-go at €0.007) | about €500 (Premium + 40,000 at €0.005), or custom |
| Gotenberg | $0 licence + your servers | $0 licence + your servers | $0 licence + your servers |
| Browserless cloud (annual billing, read 2026-10-14) | Free tier (1,000 units) | $25 (Prototyping, 20,000 units) | $140 (180,000 units) |
| RenderScreenshot | $10 | $40 | Above our largest plan (60,000 credits) |
How to read it:
- Api2Pdf has no plans: $1 a month plus $0.001 per MB and $0.00019551 per second of compute (read 2026-10-15). The figures come from the cost model in PDFShift vs Api2Pdf; slow or heavy documents cost more.
- PDFShift counts one credit per started 5 MB, so large PDFs cost multiples.
- PDFMonkey prices are in euros, excluding VAT. Its Boost Packs (1,000 documents for €5) cannot be combined with pay-as-you-go and may beat the 10,000 figure.
- Browserless bills units of up to 30 seconds of browser time. A short PDF is about one unit; monthly billing costs more than the annual rates shown.
- RenderScreenshot charges one credit per successful PDF at any size and stops at the limit instead of billing overage. At 100,000 a month we are not a self-serve option today.
- Gotenberg is free to run, but you pay for CPU, memory, upgrades and the person on call.
The seven, one by one
DocRaptor
DocRaptor is a hosted Prince service. You POST HTML or a URL to https://api.docraptor.com/docs with your key as the HTTP Basic user name, and you get the PDF back. Its API reference exposes Prince's controls: PDF/A and PDF/UA profiles, passwords and permissions, and a pipeline field that picks the Prince version.
- Strengths: the best paged-media support in this list; PDF/A and PDF/UA; Excel output; async jobs up to 600 seconds; unlimited free watermarked test documents.
- Weaknesses: a second engine to design for (in our earlier test its default pipeline stacked a CSS grid into one column); print CSS applied by default; JavaScript off by default; the highest prices here.
- Price: $75, $399 and enterprise at 1k, 10k and 100k a month.
# DocRaptor, test mode (watermarked, free). Not run for this page; run on 2026-10-15 for our comparison. curl --fail -u "$DOCRAPTOR_API_KEY:" -H 'Content-Type: application/json' \ -d '{"test": true, "type": "pdf", "document_content": "<h1>Invoice 2048</h1>"}' \ https://api.docraptor.com/docs -o docraptor.pdf
More: DocRaptor alternative and DocRaptor vs PDFShift.
PDFShift
PDFShift wraps Chromium behind one endpoint, POST https://api.pdfshift.io/v3/convert/pdf, authenticated with an X-API-Key header. Its convert reference lists flat options: format, margin, use_print, header and footer templates with {{page}} and {{total}}, waits and security settings.
- Strengths: Chrome output; you can pin the Chromium build (
X-Processor-Version: 116or142); passwords, watermarks, HIPAA and GDPR switches; a sandbox flag for free tests; a list price at 100,000. - Weaknesses: credits per started 5 MB; coarse plan steps; the version you pin decides which CSS works (Chrome 128 in our test printed no CSS page counters, 137 and later did).
- Price: $24, $99 and $249.
# PDFShift, sandbox (watermarked, free). Not run: we have no PDFShift account. curl --fail -H "X-API-Key: $PDFSHIFT_API_KEY" -H 'Content-Type: application/json' \ -d '{"source": "<h1>Invoice 2048</h1>", "format": "A4", "sandbox": true}' \ https://api.pdfshift.io/v3/convert/pdf -o pdfshift.pdf
More: PDFShift alternative, with a translator for its request body.
Api2Pdf
Api2Pdf sells several engines behind one key: Chrome endpoints such as /chrome/pdf/html, plus wkhtmltopdf and LibreOffice endpoints and PDF tools (merge, passwords). The raw API key goes in the Authorization header with no "Bearer" prefix. The response is JSON with a FileUrl that Api2Pdf deletes after 24 hours. If you are moving off it, the Api2Pdf alternative page maps every Chrome option.
- Strengths: pay-per-use pricing that is hard to beat for small documents; Office conversion and PDF tools in the same account; a larger "XL" host for heavy pages.
- Weaknesses: a second request to fetch the file; the bill grows with render seconds, so slow pages cost more; Chrome options are nested in camelCase under
options. - Price: about $1.59, $6.91 and $60.10 for small, fast documents.
# Api2Pdf, Chrome engine. Not run: we have no Api2Pdf account. curl --fail -H "Authorization: $API2PDF_API_KEY" -H 'Content-Type: application/json' \ -d '{"html": "<h1>Invoice 2048</h1>", "inline": false, "options": {"printBackground": true}}' \ https://v2.api2pdf.com/chrome/pdf/html # The JSON response has FileUrl; download it within 24 hours.
PDFMonkey
PDFMonkey works differently from the rest: you design a template in its dashboard (HTML, CSS and Liquid), and each API call sends only the data. Its documents API creates a document from document_template_id and a JSON payload. Generation is async by default; a /documents/sync endpoint waits up to 6 minutes. Templates run on an engine you pick; the default "v5" engine is based on Chrome 133, per its engines page.
- Strengths: templates live outside your codebase, so non-developers can edit them; a builder for simple layouts; old templates keep the engine they were made with.
- Weaknesses: you cannot just POST arbitrary HTML per request; download links expire after 1 hour; the free plan cannot load external images, fonts, CSS or JavaScript; Chrome 133 is older than most Chrome-based services here.
- Price: €15, about €64 and about €500 (or custom above 60,000).
# PDFMonkey, synchronous call with a stored template. Not run: we have no PDFMonkey account. curl --fail -H "Authorization: Bearer $PDFMONKEY_SECRET_KEY" -H 'Content-Type: application/json' \ -d '{"document": {"document_template_id": "YOUR-TEMPLATE-UUID", "status": "pending", "payload": {"invoice_number": "2048", "total": "4,912.00"}}}' \ https://api.pdfmonkey.io/api/v1/documents/sync # Read document_card.download_url from the response and fetch it within the hour.
Gotenberg
Gotenberg is an open-source container (MIT) that puts Chromium and LibreOffice behind HTTP routes. You POST a multipart form whose main file must be called index.html. In our Gotenberg vs Browserless test (2026-10-15, Gotenberg 8.37.0), warm PDF requests took 75 to 130 ms on a laptop and the container idled at about 224 MiB.
- Strengths: no per-document fee; fast; Office-to-PDF, merge and PDF/A in the same image; nothing leaves your network.
- Weaknesses: you run, patch, scale and monitor it; fonts are whatever you install in the image; no hosted option.
- Price: licence free at every volume; budget for the servers.
# Gotenberg, local container: docker run --rm -p 3000:3000 gotenberg/gotenberg:8 # Run against a local container on 2026-10-15 for our Gotenberg vs Browserless comparison. curl --fail --form files=@index.html --form printBackground=true \ http://localhost:3000/forms/chromium/convert/html -o gotenberg.pdf
Browserless
Browserless hosts Chrome. Its REST API has a /pdf route that takes html or url and Puppeteer-style options, and it also accepts your own Puppeteer or Playwright scripts over a WebSocket. That second mode is the reason to choose it: log in, click, scroll, then print.
- Strengths: full browser control; the same API on their cloud or in your container; cheap units for short jobs.
- Weaknesses: you write and maintain the printing logic; the container image is dual-licensed (SSPL or commercial), so check the terms before you self-host in a closed product; fonts in the container shape your page count (our report printed 11 pages there, 6 on a Mac).
- Price: free tier, $25 and $140 a month on annual billing.
# Browserless /pdf. We ran this request against a local container (Browserless 2.57.0, Chrome 156) on 2026-10-18. curl --fail -H 'Content-Type: application/json' \ -d '{"html": "<h1>Invoice 2048</h1>", "options": {"format": "A4", "printBackground": true}}' \ "http://localhost:3000/pdf?token=$BROWSERLESS_TOKEN" -o browserless.pdf
RenderScreenshot
RenderScreenshot is a screenshot API that also prints PDFs. One endpoint takes a URL, HTML or Markdown; output.format: "pdf" turns on the PDF options: paper, margins, scale, page ranges, header and footer templates, CSS page size and fit-to-one-page. Every response has an X-Cache-URL header, a public CDN link to the same file.
- Strengths: PDFs, PNG, JPEG and WebP from one key; a shareable link without your own storage; one credit per PDF at any size; repeat renders inside the cache TTL cost 0 credits with
cache.available; SDKs in six languages. - Weaknesses: Chrome only, so no footnotes or page references; Letter is the default paper and
screenthe default media, which surprises people coming from DocRaptor or PDFShift; no PDF passwords or watermarks; a 60-second wait limit; no self-serve plan at 100,000 a month. - Price: $10 and $40 at 1k and 10k; not self-serve at 100k.
# RenderScreenshot. Follows the API reference; not run against the live API for this page. curl --fail -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" -H 'Content-Type: application/json' \ -d '{"html": "<h1>Invoice 2048</h1>", "output": {"format": "pdf"}, "pdf": {"paper": "a4", "margin": "15mm"}}' \ -o renderscreenshot.pdf
More: the PDF generation API page covers header and footer templates and a print-CSS checklist.
Not on the list: wkhtmltopdf
wkhtmltopdf still appears in many "HTML to PDF" answers, and some APIs still offer it as an engine. Its repository was archived in January 2023, and in our test it failed most modern CSS. Distribution packages also silently ignore header, footer and print-media flags. If you run it today, read the wkhtmltopdf alternative guide, which has drop-in shims for Python, Ruby and PHP.
Scored checklist
Each cell is from the vendor's docs, read in October 2026. "Yes" means the feature is a documented request option. "Not checked" means we did not find it in the pages we read, which is not proof that it is missing.
| Requirement | DocRaptor | PDFShift | Api2Pdf | PDFMonkey | Gotenberg | Browserless | RenderScreenshot |
|---|---|---|---|---|---|---|---|
| Chrome engine | No (Prince) | Yes | Yes | Yes | Yes | Yes | Yes |
| Footnotes and page references | Yes | No | No | No | No | No | No |
| Raw HTML per request | Yes | Yes | Yes | No (stored templates) | Yes | Yes | Yes |
| PDF bytes in the response | Yes | Yes | No (link) | No (link) | Yes | Yes | Yes |
| Header and footer page numbers | CSS | Templates | Templates | Not checked | Files | Templates | Templates and CSS |
| Async with callback | Yes | Yes | Not checked | Yes | Yes | No | Yes |
| Images from the same API | No | Yes | Yes | Not checked | Yes | Yes | Yes |
| PDF passwords | Yes | Yes | Yes (PDF tools) | Not checked | Not checked | No | No |
| Self-host option | No | No | No | No | Yes | Yes | No |
| Free watermarked test mode | Yes | Yes | No (free credit instead) | No (free plan instead) | n/a | n/a | No |
Run your own evaluation
Our engine test cannot tell you how a hosted service performs from your region with your templates. This harness can. It sends each template N times through one provider and prints a CSV row with the first-call time, warm p50 and p95, failures, page count and bytes. Put a few real templates in a folder, run it once per provider, and compare the rows.
// Time any HTML-to-PDF provider on your own templates: node harness.mjs <provider> <runs> templates/*.html // Prints one CSV row per template: provider, file, first ms, p50 ms, p95 ms, failures, pages, bytes. import { readFile } from 'node:fs/promises'; import { basename } from 'node:path'; const providers = { // Self-hosted baseline. npm i puppeteer async puppeteer(html) { const { default: puppeteer } = await import('puppeteer'); providers.browser ??= await puppeteer.launch(); const page = await providers.browser.newPage(); try { await page.setContent(html, { waitUntil: 'networkidle0' }); return Buffer.from(await page.pdf({ format: 'A4', printBackground: true, preferCSSPageSize: true })); } finally { await page.close(); } }, async renderscreenshot(html) { return post('https://api.renderscreenshot.com/v1/screenshot', { Authorization: `Bearer ${process.env.RS_API_KEY}` }, { html, output: { format: 'pdf' }, pdf: { paper: 'a4', prefer_css_page_size: true }, wait: { until: 'networkidle' } }); }, async pdfshift(html) { return post('https://api.pdfshift.io/v3/convert/pdf', { 'X-API-Key': process.env.PDFSHIFT_API_KEY }, { source: html, format: 'A4', sandbox: true }); }, async docraptor(html) { const auth = Buffer.from(`${process.env.DOCRAPTOR_API_KEY}:`).toString('base64'); return post('https://api.docraptor.com/docs', { Authorization: `Basic ${auth}` }, { type: 'pdf', document_content: html, test: true }); }, }; async function post(url, headers, body) { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify(body), signal: AbortSignal.timeout(90_000), }); if (!res.ok) throw new Error(`${res.status} ${(await res.text()).slice(0, 200)}`); return Buffer.from(await res.arrayBuffer()); } const pct = (sorted, p) => sorted[Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1)]; const pages = (pdf) => (pdf.toString('latin1').match(/\/Type\s*\/Page[^s]/g) ?? []).length; const [name, runsArg, ...files] = process.argv.slice(2); const render = providers[name]; if (!render || !files.length) throw new Error('usage: node harness.mjs <puppeteer|renderscreenshot|pdfshift|docraptor> <runs> <files...>'); console.log('provider,file,first_ms,p50_ms,p95_ms,failures,pages,bytes'); try { for (const file of files) { const html = await readFile(file, 'utf8'); const times = []; let failures = 0; let last; for (let i = 0; i < Number(runsArg); i++) { const t0 = performance.now(); try { last = await render(html); times.push(performance.now() - t0); } catch (err) { failures++; console.error(`${file} run ${i + 1}: ${err.message}`); } } const warm = times.slice(1).sort((a, b) => a - b); console.log([name, basename(file), Math.round(times[0] ?? NaN), Math.round(pct(warm, 50) ?? NaN), Math.round(pct(warm, 95) ?? NaN), failures, last ? pages(last) : 0, last?.length ?? 0].join(',')); if (failures) process.exitCode = 1; } } finally { await providers.browser?.close(); }
We ran the puppeteer provider on 2026-10-18 (Node 22.18.0, Puppeteer 24.43.1, six runs per template); that run is where the networkidle0 figures above come from. The other providers follow each vendor's documented request shape and were not run for this page. Adding Api2Pdf or PDFMonkey means one more function that downloads the returned link. Keep DocRaptor's test: true and PDFShift's sandbox: true while you evaluate, so the runs cost nothing there.
Three rules make the comparison fair:
- Same templates, same fonts. Load web fonts from a URL in every template, so no service falls back to a font of its own.
- Same wait. Use each service's network-idle option, or none for all of them.
- Separate the first call. The harness reports it on its own; a cold start says little about steady traffic.
Changelog
- 2026-10-18: First version. Engine test with Chrome 148, Chrome 156 (Browserless), Prince 17 and wkhtmltopdf 0.12.6.1; prices read from vendor pages the same day (Browserless on 2026-10-14, Api2Pdf on 2026-10-15). Hosted services not yet run through the harness.
To add RenderScreenshot as a row in your own run, get a free API key and run the harness with renderscreenshot.
Engine tests ran on 2026-10-18 on an Apple M1 Pro: Node 22.18.0, Puppeteer 24.43.1 with Chrome 148.0.7778.97; Browserless 2.57.0 (Chrome 156.0.8078.4) and wkhtmltopdf 0.12.6.1 (with patched qt) in Docker; Prince 17 from prince-pdf 17.0.0.post2. Engine times are local and are not hosted-API latency.
Related in this topic
- HTML to PDF API: Paper, Margins, Page Numbers Turn a URL or your HTML into a PDF with real Chrome: paper sizes, margins, header and footer templates with page numbers, one-page fit and print CSS.
- DocRaptor Alternative: Prince vs Chrome, Feature by Feature We printed one paged-media test file with Prince 17 and Chrome 148. See which print features Chrome lacks, where Prince wins, and DocRaptor migration code.
- PDFShift Alternative: Option Map, Prices and Translator Move off PDFShift without rewriting calls: a tested translator for its request body in Node, Python and Ruby, prices at three volumes and Chrome versions.
- wkhtmltopdf Alternative: Tested Migration to Chrome wkhtmltopdf is archived. We ran two builds on modern CSS, found flags a distro build ignores, and wrote drop-in shims for pdfkit, WickedPDF and Snappy.
- Api2Pdf Alternative: Option Map, Defaults and Migration Moving off Api2Pdf? Map every Chrome PDF option, catch the three defaults that change your output, compare costs at 3 volumes, and use tested adapters.
- HTML to PDF in Node.js: Puppeteer, Playwright, PDFKit HTML to PDF in Node.js with Puppeteer, Playwright, PDFKit or an API, tested on an invoice and a chart report: times, file sizes, page numbers and pitfalls.