PDF generation is one of those features that seems simple until you actually build it. Invoices, reports, contracts, receipts, documentation exports — every SaaS eventually needs to generate PDFs from dynamic content. The challenge is that HTML rendering in PDF context is full of edge cases: page breaks, headers/footers, font embedding, background graphics, and CSS print media queries.

This guide covers every approach to HTML-to-PDF conversion in 2026: from low-level libraries to browser-based rendering to API services that handle everything for you.

Three Approaches to PDF Generation

There are fundamentally three ways to generate PDFs from HTML:

  • Library-based: PDFKit, jsPDF, pdfmake — you construct the PDF programmatically. Full control but no HTML rendering.
  • Browser-based: Playwright/Puppeteer page.pdf() — renders HTML in a real browser, then prints to PDF. Best fidelity but requires browser infrastructure.
  • API-based: Send a URL or HTML string, get back a PDF. No infrastructure to manage.

Library Approach — PDFKit

PDFKit gives you low-level control over every element in the PDF. Great for structured documents like invoices where you control the exact layout:

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

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

  // Header
  doc.fontSize(20).text('INVOICE', 50, 50);
  doc.fontSize(10)
    .text(`Invoice #: ${data.number}`, 50, 80)
    .text(`Date: ${data.date}`, 50, 95)
    .text(`Due: ${data.due}`, 50, 110);

  // Customer info
  doc.fontSize(12).text('Bill To:', 50, 150);
  doc.fontSize(10)
    .text(data.customer.name, 50, 170)
    .text(data.customer.address, 50, 185)
    .text(data.customer.email, 50, 200);

  // Line items table
  let y = 250;
  doc.fontSize(10).font('Helvetica-Bold');
  doc.text('Item', 50, y);
  doc.text('Qty', 300, y);
  doc.text('Price', 370, y);
  doc.text('Total', 450, y);

  doc.moveTo(50, y + 15).lineTo(550, y + 15).stroke();
  y += 25;

  doc.font('Helvetica');
  for (const item of data.items) {
    doc.text(item.description, 50, y);
    doc.text(item.quantity.toString(), 300, y);
    doc.text(`$${item.price.toFixed(2)}`, 370, y);
    doc.text(`$${(item.quantity * item.price).toFixed(2)}`, 450, y);
    y += 20;
  }

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

  doc.end();
  return new Promise(resolve => stream.on('finish', resolve));
}
The limitation: PDFKit doesn't render HTML or CSS. Every element must be positioned manually. For complex layouts with tables, images, and styled text, this becomes hundreds of lines of code. If your content is already in HTML (like a web page or email template), you need a different approach.

Browser Approach — Playwright page.pdf()

Playwright's page.pdf() renders HTML in a real Chromium browser and exports to PDF. This gives you pixel-perfect rendering of any HTML/CSS:

import { chromium } from 'playwright';

async function htmlToPdf(options: {
  url?: string;
  html?: string;
  format?: string;
  margin?: { top: string; bottom: string; left: string; right: string };
  printBackground?: boolean;
  headerTemplate?: string;
  footerTemplate?: string;
}): Promise<Buffer> {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  if (options.url) {
    await page.goto(options.url, { waitUntil: 'networkidle' });
  } else if (options.html) {
    await page.setContent(options.html, { waitUntil: 'networkidle' });
  }

  const pdf = await page.pdf({
    format: options.format ?? 'A4',
    printBackground: options.printBackground ?? true,
    margin: options.margin ?? {
      top: '1cm', bottom: '1cm',
      left: '1cm', right: '1cm',
    },
    displayHeaderFooter: !!(options.headerTemplate || options.footerTemplate),
    headerTemplate: options.headerTemplate ?? '',
    footerTemplate: options.footerTemplate ??
      '<div style="font-size:8px;text-align:center;width:100%;">' +
      'Page <span class="pageNumber"></span> of ' +
      '<span class="totalPages"></span></div>',
  });

  await browser.close();
  return pdf;
}

Print CSS for Clean PDFs

@media print {
  /* Hide non-printable elements */
  nav, footer, .sidebar, .cookie-banner { display: none !important; }

  /* Control page breaks */
  h2, h3 { page-break-after: avoid; }
  table, figure { page-break-inside: avoid; }
  .page-break { page-break-before: always; }

  /* Ensure backgrounds print */
  * { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }

  /* Readable link URLs */
  a[href]::after { content: " (" attr(href) ")"; font-size: 0.8em; color: #666; }
}
Pro tip: Always add print-color-adjust: exact to preserve background colors and images in PDFs. Without it, browsers strip backgrounds by default for print.

API Approach — One Request, Professional PDF

A PDF generation API handles browser management, font rendering, and all the edge cases. Send a URL or HTML, get back a PDF:

Generate PDF from URL

const response = await fetch('https://api.snapapi.pics/v1/pdf', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'sk_live_your_key_here',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://myapp.com/invoice/12345',
    format: 'A4',
    print_background: true,
    margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' },
  }),
});

const pdfBuffer = await response.arrayBuffer();
// Save or email the PDF
fs.writeFileSync('invoice.pdf', Buffer.from(pdfBuffer));

Generate PDF from Raw HTML

const invoiceHtml = `
<html>
<head>
  <style>
    body { font-family: 'Helvetica', sans-serif; padding: 40px; }
    .header { display: flex; justify-content: space-between; margin-bottom: 40px; }
    .logo { font-size: 24px; font-weight: bold; color: #6366f1; }
    table { width: 100%; border-collapse: collapse; margin: 20px 0; }
    th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
    th { background: #f8fafc; font-weight: 600; }
    .total { font-size: 18px; font-weight: bold; text-align: right; margin-top: 20px; }
  </style>
</head>
<body>
  <div class="header">
    <div class="logo">Acme Corp</div>
    <div>Invoice #INV-2026-001<br>Date: April 5, 2026</div>
  </div>
  <table>
    <tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr>
    <tr><td>API Pro Plan</td><td>1</td><td>$79.00</td><td>$79.00</td></tr>
    <tr><td>Extra requests (10K)</td><td>2</td><td>$15.00</td><td>$30.00</td></tr>
  </table>
  <div class="total">Total: $109.00</div>
</body>
</html>`;

const response = await fetch('https://api.snapapi.pics/v1/pdf', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'sk_live_your_key_here',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    html: invoiceHtml,
    format: 'A4',
    print_background: true,
  }),
});

Common PDF Generation Use Cases

Invoice Generation

Generate invoices from order data by rendering an HTML template and converting to PDF. Use URL mode if you have an invoice page, or HTML mode to render templates server-side with Handlebars, EJS, or React.

Report Export

Let users download dashboard data as PDF reports. Screenshot the dashboard at full page, or build a print-optimized template that includes charts, tables, and summaries.

Contract & Legal Documents

Generate contracts from templates with variable data (names, dates, amounts). Use print CSS to control page breaks and ensure legal formatting requirements.

Receipt Generation

Generate receipts after purchases or donations. Use HTML mode with a receipt template — faster than URL mode since there's no page to load.

Documentation Export

Let users download documentation as PDF. Capture the docs page with full-page rendering and proper print styles for clean output.

Python Example

import requests

# Generate PDF from URL
response = requests.post(
    'https://api.snapapi.pics/v1/pdf',
    headers={'X-Api-Key': 'sk_live_your_key_here'},
    json={
        'url': 'https://myapp.com/report/monthly',
        'format': 'A4',
        'print_background': True,
        'margin': {'top': '2cm', 'bottom': '2cm'},
    }
)

with open('report.pdf', 'wb') as f:
    f.write(response.content)
print(f"PDF generated: {len(response.content)} bytes")

Approach Comparison

Factor PDFKit (Library) Playwright (Browser) SnapAPI (API)
HTML rendering ❌ No HTML support ✅ Full browser rendering ✅ Full browser rendering
CSS support ❌ Manual styling ✅ Full CSS + print media ✅ Full CSS + print media
Infrastructure None (npm package) Chromium binary required None (API call)
Page headers/footers Manual positioning ✅ Template-based ✅ Template-based
Web fonts Manual embedding Requires font installation ✅ Automatic
JavaScript rendering
URL to PDF ✅ With navigation code ✅ Single parameter
Best for Simple, structured docs Full control, self-hosted Speed, scale, no infra

Getting Started with SnapAPI PDF Generation

  1. Sign up free at snapapi.pics — 200 requests/month included
  2. Get your API key from the dashboard
  3. Generate your first PDF — pass a URL or raw HTML
  4. Install an SDK — JavaScript, Python, Go, PHP, Swift, Kotlin, and more

Generate PDFs Without Infrastructure

URL to PDF. HTML to PDF. Custom margins, headers, footers. 200 free requests/month. No credit card required.

Get Your Free API Key