Migrate from wkhtmltopdf to Papyr
wkhtmltopdf was archived in January 2023 and has an unpatched server-side request forgery vulnerability rated CVSS 9.8. If it's in your stack, you need to replace it. Papyr takes the same HTML input and returns a PDF — migration takes about 15 minutes.
Why wkhtmltopdf has to go
Archived — no more patches
The maintainers archived the project on GitHub in January 2023. No new releases, no security fixes, no bug fixes. The last release was 0.12.6 in 2020.
CVSS 9.8 SSRF vulnerability (CVE-2022-35583)
An unpatched server-side request forgery vulnerability allows attackers to reach your internal network via crafted HTML input. If users control any part of the HTML you render, this is a live attack vector.
No modern CSS
wkhtmltopdf uses QtWebKit, which is stuck at roughly Chrome 28 (2013). Flexbox is broken. CSS Grid doesn't exist. Custom properties don't work. You spend hours writing table-based HTML for a 2024 app.
Binary dependency hell
Font rendering differences between macOS and Linux. Missing system fonts on Alpine. Locale issues. Different output across environments.
Migration guide
Your HTML stays exactly the same. You replace the wkhtmltopdf call with an HTTP request.
Before — wkhtmltopdf CLI or library
Typical wkhtmltopdf usage (shell exec, node-wkhtmltopdf, pdfkit wrapper, etc.):
wkhtmltopdf --page-size A4 --margin-top 20mm input.html output.pdfAfter — Papyr API (Node.js)
const fs = require("fs");
async function htmlToPdf(html) {
const res = await fetch("https://api.getpapyr.dev/v1/render/pdf", {
method: "POST",
headers: {
"Authorization": "Bearer pk_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({ html, options: { format: "A4" } }),
});
return Buffer.from(await res.arrayBuffer());
}
const pdf = await htmlToPdf("<h1>Hello</h1>");
fs.writeFileSync("output.pdf", pdf);After — Python
import requests
def html_to_pdf(html: str) -> bytes:
res = requests.post(
"https://api.getpapyr.dev/v1/render/pdf",
headers={"Authorization": "Bearer pk_live_..."},
json={"html": html, "options": {"format": "A4"}},
)
res.raise_for_status()
return res.content
pdf = html_to_pdf("<h1>Hello</h1>")
with open("output.pdf", "wb") as f:
f.write(pdf)After — PHP
$ch = curl_init("https://api.getpapyr.dev/v1/render/pdf");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer pk_live_...",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"html" => "<h1>Hello</h1>",
"options" => ["format" => "A4"],
]),
]);
$pdf = curl_exec($ch);
file_put_contents("output.pdf", $pdf);Options mapping
| wkhtmltopdf flag | Papyr option |
|---|---|
| --page-size A4 | options.format: "A4" |
| --orientation Landscape | options.landscape: true |
| --margin-top 20mm | options.margin.top: "20mm" |
| --margin-right 15mm | options.margin.right: "15mm" |
| --margin-bottom 20mm | options.margin.bottom: "20mm" |
| --margin-left 15mm | options.margin.left: "15mm" |
| --no-background | options.printBackground: false |
What gets better immediately
Ready to migrate?
Free tier includes 100 documents/month. No credit card required.