PDFs from code,
the way it should be.
paperjet renders Typst → PDF in under 100 ms. Real templates with JSON data merge. Page-perfect breaks. No headless Chrome.
Free 100 PDFs / month. No credit card to start.
# Render an invoice PDF in one HTTP call.
curl -X POST https://api.paperjet.dev/v1/render \
-H "Authorization: Bearer $PAPERJET_API_KEY" \
-H "Content-Type: application/json" \
-o invoice.pdf \
-d '{
"template_id": "invoice-v1",
"data": {
"invoice": { "number": "INV-2026-0042" },
"items": [
{ "description": "Pro plan", "unit_price": 29, "quantity": 1 }
]
}
}' The PDF API for people who ship.
HTML→PDF was a workaround. Typst is a real layout engine designed for documents. The result: cleaner output, less plumbing, faster runs.
~75 ms warm renders
Compute is a Rust + axum binary inside a Cloudflare Container. No Chrome process boot, no V8 warmup. Wire-time of an HTTP call, end of story.
Page-perfect breaks
Pagination is a first-class concern in Typst's layout engine.
No widow/orphan hacks, no page-break-inside: avoid
incantations, no surprise blank pages.
Templates with data merge
Upload a Typst template once, render it from any backend with
a JSON data payload. Real branching,
loops, math. Not string replacement.
One template. Every customer.
Drop a .typ
file in your R2 bucket. Reference it by id. Send your data as JSON. Get the PDF back.
Templates are real Typst — you have if,
for,
functions, math, tables, your fonts. We just inject your data
as a top-level data binding.
// data is whatever JSON you POST. Pure Typst from here.
#set page(margin: 2cm)
#text(size: 22pt, weight: "bold")[
Invoice #data.invoice.number
]
*Bill to:* #data.to.name
*Date:* #data.invoice.date
#table(
columns: (1fr, auto, auto),
[*Item*], [*Qty*], [*Total*],
..data.items.map(it => (
[#it.description],
[#it.quantity],
["€" + str(it.unit_price * it.quantity)],
)).flatten()
) From zero to PDF in 30 seconds.
TypeScript SDK, OpenAPI spec, or a vanilla curl call.
Pick the integration that fits.
import { Paperjet } from "@paperjet/sdk";
const pj = new Paperjet({
apiKey: process.env.PAPERJET_API_KEY!,
});
const pdf = await pj.render({
template_id: "invoice-v1",
data: { /* anything JSON-shaped */ },
});
await Bun.write("out.pdf", pdf); curl -X POST https://api.paperjet.dev/v1/render \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"source":"= Hello #data.name","data":{"name":"World"}}' \
-o hello.pdf
# 200 OK · application/pdf · 11 KB · 75 ms.
# Stripe-style Idempotency-Key prevents
# double-render on retries (24 h replay). Pricing that doesn't punish growth.
Free tier with a hard cap so a runaway loop never lands a four-figure bill. Paid plans bill predictable monthly with cents-per-PDF overage.
Ready to ship better PDFs?
No credit card. Free 100 renders/month forever.