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

OptionExample
format"Letter", "A4", "Legal"…
landscapetrue
margin{"top":"1in","bottom":"20mm"}
footerTemplatepage 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.