wkhtmltopdf served a generation of apps well. But its GitHub repository was archived in January 2023, and it renders with a QtWebKit engine from the mid-2010s. Flexbox is unreliable, CSS grid and CSS variables don't work, modern JavaScript fails silently, and nobody ships security fixes anymore.
| Option | Good for | Trade-off |
|---|---|---|
Puppeteer / Playwright page.pdf() | Full control, same engine as Chrome | You run and patch headless Chromium (~300MB, memory spikes, fonts) |
| Gotenberg (Docker) | Self-hosted HTTP API around Chromium and LibreOffice | One more service to deploy, scale and update |
| WeasyPrint (Python) | Excellent CSS paged media: running headers, page counters | Its own engine: no JavaScript, some layout differences from browsers |
| A hosted API (Shotmint, PDFShift, …) | No browser to operate, one HTTP call | Your HTML goes over the network to a third party; costs per document |
If you need JavaScript-rendered content and browser-identical layout, choose a Chromium option. If you need advanced print features (running headers, footnotes, counter(page)), WeasyPrint is hard to beat.
# Before: wkhtmltopdf
wkhtmltopdf --page-size A4 -O Landscape --print-media-type input.html out.pdf
# After: one HTTP call (Chromium)
curl -X POST https://shotmint.creasework.com/v1/pdf \
-H "X-API-Key: $SHOTMINT_KEY" -H "Content-Type: application/json" \
--data-binary @<(jq -Rs '{html: ., format: "A4", landscape: true}' input.html) \
-o out.pdf
# Before
import pdfkit
pdfkit.from_string(html, "out.pdf", options={"page-size": "Letter", "margin-top": "15mm"})
# After
import os, requests
r = requests.post("https://shotmint.creasework.com/v1/pdf",
headers={"X-API-Key": os.environ["SHOTMINT_KEY"]},
json={"html": html, "format": "Letter", "margin": "15mm"}, timeout=60)
r.raise_for_status()
open("out.pdf", "wb").write(r.content)
const r = await fetch("https://shotmint.creasework.com/v1/pdf", {
method: "POST",
headers: { "X-API-Key": process.env.SHOTMINT_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ html, format: "A4", margin: "10mm" }),
});
if (!r.ok) throw new Error(await r.text());
require("fs").writeFileSync("out.pdf", Buffer.from(await r.arrayBuffer()));
| wkhtmltopdf | Shotmint /v1/pdf |
|---|---|
--page-size A4 | format=A4 (A3, A4, A5, Letter, Legal, Tabloid) |
-O Landscape | landscape=true |
-T -B -L -R 10mm | margin=10mm (all sides). For different sides, pass margin=0mm and set @page { margin: … } in CSS |
--no-background | print_background=false |
--zoom 0.8 | scale=0.8 |
| default (screen CSS) | screen_media=true |
--print-media-type | default (print CSS) |
--javascript-delay 1000 | delay=1000, or wait_until=networkidle |
--custom-header User-Agent … | user_agent=… |
--header-html, --footer-html, --toc | Not supported. Put the header in your HTML; a <thead> repeats on every page |
--print-media-type. Chromium uses print CSS. If the output suddenly hides elements, check your @media print rules or pass screen_media=true.pt or tune scale.file:// images and stylesheets can't be reached from a hosted renderer. Use absolute https:// URLs, or inline them as data URIs and <style>.page-break-* properties still work; the modern ones are break-before: page and break-inside: avoid. See the HTML invoice guide for print CSS tips.Want to see the output first? Paste HTML into the free HTML to PDF converter (no key needed).
One PDF = one credit, whatever the page count. €5 buys 2,000 PDFs, credits never expire and there's no subscription. Failed renders are refunded automatically. Compare that with running a Chromium container around the clock for a few hundred documents a month.
No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire).
Get a free API key Full API reference