Beta — built on Typst 0.14, Cloudflare Containers, axum

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.

curl
# 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 }
    ]
  }
}'
<100 ms
render p95 (warm)
1 KB
minimum invoice size
EU-resident
D1, R2, Container
100% Typst
no Chrome, no Puppeteer

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.

Templates

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.

invoice-v1.typ
// 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.

TypeScript / Node / Bun @paperjet/sdk
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);
Anything that speaks HTTP REST
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.

€0 — 100 PDFs / mo
€9 — 1 000 PDFs / mo
€29 — 10 000 + PDF/UA-1
€99 — 50 000 + priority
Full pricing →

Ready to ship better PDFs?

No credit card. Free 100 renders/month forever.