PDF Generation

How to Automate PDF Generation with an API in 2026

Published April 5, 2026 · 15 min read

PDF generation is a critical feature for SaaS products — invoices, reports, contracts, receipts, certificates. The challenge is rendering complex HTML layouts with proper fonts, pagination, headers, footers, and page breaks into pixel-perfect PDFs. This guide covers every approach: from code-first libraries to browser-based rendering to managed APIs.

PDF Generation Approaches

There are three main categories of PDF generation tools, each with different trade-offs:

Code-First: PDFKit

PDFKit generates PDFs with imperative drawing commands. Best for simple, structured documents where you control every element:

import PDFDocument from 'pdfkit';
import fs from 'fs';

function generateInvoice(invoice) {
  const doc = new PDFDocument({ margin: 50 });
  doc.pipe(fs.createWriteStream(`invoice-${invoice.number}.pdf`));

  // Header
  doc.fontSize(20).text('INVOICE', { align: 'right' });
  doc.fontSize(10).text(`#${invoice.number}`, { align: 'right' });
  doc.moveDown();

  // Company info
  doc.fontSize(12).text(invoice.company);
  doc.fontSize(10)
    .text(invoice.address)
    .text(invoice.email)
    .moveDown(2);

  // Bill to
  doc.fontSize(12).text('Bill To:');
  doc.fontSize(10)
    .text(invoice.customer.name)
    .text(invoice.customer.email)
    .moveDown(2);

  // Table header
  const tableTop = doc.y;
  doc.fontSize(10).font('Helvetica-Bold');
  doc.text('Item', 50, tableTop);
  doc.text('Qty', 300, tableTop);
  doc.text('Price', 370, tableTop);
  doc.text('Total', 450, tableTop);

  doc.moveTo(50, tableTop + 15)
    .lineTo(550, tableTop + 15)
    .stroke();

  // Table rows
  let y = tableTop + 25;
  doc.font('Helvetica');

  for (const item of invoice.items) {
    doc.text(item.name, 50, y);
    doc.text(item.qty.toString(), 300, y);
    doc.text(`$${item.price.toFixed(2)}`, 370, y);
    doc.text(`$${(item.qty * item.price).toFixed(2)}`, 450, y);
    y += 20;
  }

  // Total
  doc.moveTo(370, y + 5).lineTo(550, y + 5).stroke();
  doc.font('Helvetica-Bold')
    .text(`Total: $${invoice.total.toFixed(2)}`, 370, y + 15);

  doc.end();
}

PDFKit works but gets verbose fast. Adding a logo, custom fonts, or responsive layouts requires manual positioning for every element. For complex documents, HTML/CSS is much more natural.

Browser-Based: Playwright page.pdf()

Playwright renders any HTML/CSS page and exports it as a PDF with full print stylesheet support:

import { chromium } from 'playwright';

async function htmlToPdf(html, outputPath) {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.setContent(html, { waitUntil: 'networkidle' });

  await page.pdf({
    path: outputPath,
    format: 'A4',
    margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' },
    printBackground: true,
    displayHeaderFooter: true,
    headerTemplate: `
      <div style="font-size:9px;width:100%;text-align:center;color:#666;">
        Invoice #12345
      </div>`,
    footerTemplate: `
      <div style="font-size:9px;width:100%;text-align:center;color:#666;">
        Page <span class="pageNumber"></span> of <span class="totalPages"></span>
      </div>`,
  });

  await browser.close();
}

// HTML template with print CSS
const invoiceHtml = `
<!DOCTYPE html>
<html>
<head>
  <style>
    @media print {
      body { font-family: 'Helvetica', sans-serif; }
      table { width: 100%; border-collapse: collapse; }
      th, td { padding: 8px 12px; border-bottom: 1px solid #eee; }
      .total-row { font-weight: bold; border-top: 2px solid #333; }
      .page-break { page-break-before: always; }
    }
  </style>
</head>
<body>
  <h1>Invoice #12345</h1>
  <!-- Full HTML invoice layout -->
</body>
</html>`;

await htmlToPdf(invoiceHtml, 'invoice.pdf');

HTML Templates with Handlebars

For production invoice/report generation, use a template engine to inject dynamic data into HTML templates:

import Handlebars from 'handlebars';
import { chromium } from 'playwright';
import fs from 'fs';

// Register helpers
Handlebars.registerHelper('currency', (val) => `$${val.toFixed(2)}`);
Handlebars.registerHelper('date', (val) =>
  new Date(val).toLocaleDateString('en-US', {
    year: 'numeric', month: 'long', day: 'numeric'
  })
);

const templateSource = fs.readFileSync('invoice-template.html', 'utf8');
const template = Handlebars.compile(templateSource);

async function generateInvoicePdf(invoiceData) {
  const html = template(invoiceData);
  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.setContent(html, { waitUntil: 'networkidle' });
  const buffer = await page.pdf({
    format: 'A4',
    printBackground: true,
    margin: { top: '2cm', bottom: '2cm', left: '1.5cm', right: '1.5cm' },
  });

  await browser.close();
  return buffer;
}

// Express endpoint
app.post('/api/invoices/:id/pdf', async (req, res) => {
  const invoice = await db.invoices.findById(req.params.id);
  const pdfBuffer = await generateInvoicePdf(invoice);

  res.setHeader('Content-Type', 'application/pdf');
  res.setHeader('Content-Disposition', `attachment; filename="invoice-${invoice.number}.pdf"`);
  res.send(pdfBuffer);
});

Managed API: SnapAPI /v1/pdf

SnapAPI's PDF endpoint handles all browser rendering, font loading, and PDF generation in a single API call. No Chromium to manage, no template engine to set up — just send a URL or HTML:

import SnapAPI from 'snapapi-js';

const snap = new SnapAPI('sk_live_your_key');

// Generate PDF from a URL
const urlPdf = await snap.pdf({
  url: 'https://yourapp.com/invoices/12345',
  format: 'A4',
  margin: { top: '2cm', bottom: '2cm', left: '1.5cm', right: '1.5cm' },
  print_background: true,
  display_header_footer: true,
});

console.log(urlPdf.url); // CDN-hosted PDF download URL

// Generate PDF from raw HTML
const htmlPdf = await snap.pdf({
  html: '<h1>Invoice #12345</h1><p>Amount: $499.00</p>',
  format: 'Letter',
  landscape: false,
});

// Use in an Express API
app.get('/api/invoices/:id/pdf', async (req, res) => {
  const invoice = await db.invoices.findById(req.params.id);
  const result = await snap.pdf({
    url: `https://yourapp.com/invoices/${invoice.id}?print=true`,
    format: 'A4',
    print_background: true,
  });
  res.redirect(result.url); // Redirect to CDN PDF
});

Approach Comparison

ApproachCSS SupportFontsSpeedInfrastructureBest For
PDFKitNone (imperative)Manual embedVery fastNoneSimple, structured docs
jsPDFNone (imperative)Manual embedFastNoneClient-side generation
wkhtmltopdfPartial (WebKit)System fontsFastBinary installSimple HTML conversion
Playwright/PuppeteerFull (Chromium)Full web fontsSlow (2-5s)Browser managementComplex layouts
SnapAPI /v1/pdfFull (Chromium)Full web fontsFast (1-3s)None (managed)Everything — zero setup

Generate PDFs from Any URL or HTML — No Setup Required

SnapAPI handles browser rendering, fonts, pagination, headers, and footers. One API call, pixel-perfect PDFs. Free tier includes 200 generations/month.

Start Free — No Credit Card Required