· Comparisons · By Simon Chiu

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: page was 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 of load, 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.

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: 116 or 142); 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 screen the 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:

  1. Same templates, same fonts. Load web fonts from a URL in every template, so no service falls back to a font of its own.
  2. Same wait. Use each service's network-idle option, or none for all of them.
  3. 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