HTML to PDF in Node.js
The usual way to make PDFs in Node is to run Puppeteer: install a 300 MB Chromium,
pin its version, keep it alive in production, and debug the 2am crashes yourself.
The other way is one fetch call to a rendering API.
This guide shows the second way.
1. Get an API key
Grab a free key (50 PDFs/month) from the homepage form or by API:
curl -X POST https://inkpdf.dev/v1/keys/free \
-H "Content-Type: application/json" \
-d '{"email":"you@company.com"}'Shortcut: the official client
npm install inkpdf — zero dependencies, full TypeScript types:
import InkPDF from 'inkpdf';
const inkpdf = new InkPDF(process.env.INKPDF_KEY);
await writeFile('invoice.pdf', await inkpdf.invoice({ to: { name: 'Northwind' }, items }));
await writeFile('report.pdf', await inkpdf.markdown('# Q2 Report', { theme: 'serif' }));2. Convert HTML to PDF
Or go dependency-free — Node 18+ has fetch built in:
import { writeFile } from 'node:fs/promises';
const res = await fetch('https://inkpdf.dev/v1/pdf', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.INKPDF_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: '<h1>Hello from Node.js</h1><p>Full CSS support included.</p>',
options: { format: 'A4' },
}),
});
if (!res.ok) throw new Error((await res.json()).message);
await writeFile('hello.pdf', Buffer.from(await res.arrayBuffer()));The response body is the PDF. Stream it to a user, upload it to S3, or attach it to an email — it's just bytes.
3. Generate an invoice from JSON (no HTML at all)
For invoices, receipts, quotes, reports, and certificates you can skip HTML entirely and post plain data against a built-in template:
const res = await fetch('https://inkpdf.dev/v1/pdf', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.INKPDF_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
template: 'invoice',
data: {
brand: { name: 'Acme Studio', color: '#0f766e' },
invoiceNumber: 'INV-2043',
issueDate: '2026-07-01',
dueDate: '2026-07-31',
currency: 'USD',
to: { name: 'Northwind Traders', address: '1 Market St, SF' },
items: [
{ description: 'Design work', quantity: 10, unitPrice: 95 },
],
taxRate: 8.5,
},
}),
});Line totals, subtotal, tax, and the grand total are computed server-side from
quantity × unitPrice.
4. Express example: PDF as a download
app.get('/invoice/:id', async (req, res) => {
const invoice = await db.getInvoice(req.params.id);
const pdf = await fetch('https://inkpdf.dev/v1/pdf', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.INKPDF_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ template: 'invoice', data: invoice }),
});
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${req.params.id}.pdf"`);
res.send(Buffer.from(await pdf.arrayBuffer()));
});Useful options
| Option | Example |
|---|---|
| format | "Letter", "A4", "Legal"… |
| landscape | true |
| margin | {"top":"1in","bottom":"20mm"} |
| footerTemplate | page numbers via <span class="pageNumber"> |
| output | "base64" to get JSON instead of raw bytes |
Full reference in the API docs. Prefer Markdown? Send
{"markdown": "# Report…", "theme": "clean"} instead of html.
Why not just run Puppeteer?
You absolutely can — it's what InkPDF runs under the hood. The trade is operational: Puppeteer means a ~300 MB browser in your deploy artifact, memory spikes per render, version pinning, and crash recovery. An API means a POST request and a monthly quota. At 2,000 renders for $19/month, most teams' time is worth more than the difference.
Try it without writing code
The playground renders live against the real pipeline — 20 free renders a day, no signup.