PDF generation in Node.js
Driving a headless browser from Node is a lot of process management for a PDF. With an HTTP API the integration is one fetch: post Markdown or HTML, receive the PDF as an ArrayBuffer, write it or send it on.
Minimal example
import { writeFile } from "node:fs/promises";
const res = await fetch("https://papertype.dev/v1/pdf", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.PAPERTYPE_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ markdown: "# Weekly report\n\n## Summary\n\nAll systems nominal.", theme: "report", cover: true, toc: true }),
});
if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).detail}`);
await writeFile("weekly-report.pdf", Buffer.from(await res.arrayBuffer()));
console.log(res.headers.get("X-Pages"), "pages;", res.headers.get("X-Quota-Used"), "of", res.headers.get("X-Quota-Limit"), "used");Works with the built-in fetch in Node 18 and later. No package to install.
HTML from your template engine
Render the template to a string and post it with theme: "none" and your print CSS. Inline images as data: URIs.
import { readFile } from "node:fs/promises";
const logo = `data:image/png;base64,${(await readFile("logo.png")).toString("base64")}`;
const html = renderInvoice({ invoice, logo }); // your template function
const css = await readFile("print.css", "utf8");
const res = await fetch("https://papertype.dev/v1/pdf", {
method: "POST", headers,
body: JSON.stringify({ html, css, theme: "none", page_size: "Letter", filename: `${invoice.number}.pdf` }),
});Sending the PDF to a browser
In an HTTP handler, forward the bytes with the right headers. Keep the API key on the server; never expose it to the client.
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Disposition", 'attachment; filename="report.pdf"');
res.end(Buffer.from(await upstream.arrayBuffer()));Errors and quotas
422: cannot render; thedetailfield says why. Fix the input rather than retrying.429: monthly quota reached; queue the job. Plans are hard-capped, so there are no surprise charges.413: source over 2 MB.401: bad key.- Concurrency: 2 renders at a time per account. Use a small queue (for example a promise pool of 2) for batch jobs.
Limits and pricing
- No JavaScript is executed. Send the final HTML; anything a script would have drawn or filled in will not appear.
- The renderer has no network access. Images, fonts and stylesheets at external URLs are never fetched; inline them as
data:URIs. A request that references an external URL fails with a 422 that names the URL, rather than silently dropping the asset. - Free-plan output carries a small
papertype.devfooter mark. Paid plans (from $9/month) do not.
Plans are capped. When a plan's monthly document count is reached the API returns 429 until you upgrade or the month rolls over; there is no overage billing.
- Free: 50 documents/month, footer mark on output, no card required.
- Starter: $9/month, 500 documents, no mark.
- Pro: $29/month, 3,000 documents.
- Scale: $79/month, 15,000 documents.
Every response includes X-Quota-Used and X-Quota-Limit headers so you can watch usage without polling. Details on the pricing page.
Questions
Is there an npm package?
An official SDK is coming. The API is one JSON endpoint, so built-in fetch is enough today and will keep working.
Can I generate PDFs from a client-side app?
Only through your own backend. The API key must stay on the server. Have your frontend call your server, which calls PaperType and returns the PDF.
Does it run my page's JavaScript?
No. Send the final HTML. If you render React or similar on the server, send the resulting markup, not the app.
How do I add fonts?
DejaVu, Liberation, Noto, Carlito and Caladea are installed. Others go in an @font-face rule with a data: URI in the css field.
Related
Free previews carry a watermark and are deleted after 24 hours. Free API output carries a footer mark. The renderer runs no JavaScript and never fetches external URLs. Privacy · Terms · Refunds