Menu
pdfjavascriptnodejstutorial

HTML to PDF API in JavaScript: Convert Web Pages to PDF Programmatically

SnapSharp Team·April 16, 2026·8 min read

Converting HTML to PDF is one of the most requested backend tasks in web development. Invoices, reports, contracts, and export features all need it. The challenge: PDF generation with full CSS support requires a headless browser. Running that yourself adds complexity.

This guide covers three approaches: an API (recommended), Puppeteer, and server-side libraries — with code examples for each.

A screenshot API turns any URL or HTML into a PDF via one HTTP call. No browser processes to manage, no memory overhead on your server, no Chromium installation.

URL to PDF

const apiKey = process.env.SNAPSHARP_API_KEY;

async function urlToPdf(url) {
  const params = new URLSearchParams({ url, format: 'pdf' });
  const res = await fetch(`https://api.snapsharp.dev/v1/screenshot?${params}`, {
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  if (!res.ok) throw new Error(`PDF generation failed: ${res.status}`);
  return Buffer.from(await res.arrayBuffer());
}

// Usage
const pdf = await urlToPdf('https://yourapp.com/invoice/INV-1234');
await fs.writeFile('invoice.pdf', pdf);

HTML to PDF

For dynamically generated content (invoices, reports, contracts), post the HTML directly:

async function htmlToPdf(html) {
  const res = await fetch('https://api.snapsharp.dev/v1/html-to-image', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ html, format: 'pdf' }),
  });

  if (!res.ok) throw new Error(`Failed: ${res.status}`);
  return Buffer.from(await res.arrayBuffer());
}

const invoiceHtml = `
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body { font-family: 'Helvetica Neue', sans-serif; padding: 40px; color: #333; }
    .header { display: flex; justify-content: space-between; margin-bottom: 40px; }
    .invoice-number { font-size: 28px; font-weight: bold; }
    table { width: 100%; border-collapse: collapse; margin-top: 20px; }
    th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e5e7eb; }
    .total { font-size: 20px; font-weight: bold; text-align: right; margin-top: 20px; }
  </style>
</head>
<body>
  <div class="header">
    <div>
      <div class="invoice-number">Invoice #INV-1234</div>
      <div style="color: #6b7280; margin-top: 8px;">April 16, 2026</div>
    </div>
    <div style="text-align: right;">
      <strong>Acme Corp</strong><br>
      123 Main Street<br>
      San Francisco, CA 94102
    </div>
  </div>
  <table>
    <thead><tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Total</th></tr></thead>
    <tbody>
      <tr><td>SnapSharp Growth Plan</td><td>1</td><td>$49.00</td><td>$49.00</td></tr>
      <tr><td>Extra screenshots</td><td>500</td><td>$0.004</td><td>$2.00</td></tr>
    </tbody>
  </table>
  <div class="total">Total: $51.00</div>
</body>
</html>
`;

const pdf = await htmlToPdf(invoiceHtml);
await fs.writeFile('invoice.pdf', pdf);

TypeScript with SDK

import SnapSharp from '@snapsharp/sdk';
import { writeFile } from 'fs/promises';

const client = new SnapSharp(process.env.SNAPSHARP_API_KEY!);

// URL to PDF
const pdf = await client.screenshot({
  url: 'https://yourapp.com/reports/monthly',
  format: 'pdf',
  fullPage: true,       // capture entire page length
  width: 1280,
});

await writeFile('report.pdf', pdf);

PDF options

const pdf = await client.screenshot({
  url: 'https://yourapp.com/invoice/INV-1234',
  format: 'pdf',
  // Paper size options:
  width: 794,           // A4 portrait: 794px @ 96dpi
  height: 1123,
  // Or letter:
  // width: 816, height: 1056,
  // Full page capture:
  fullPage: true,
  // Wait for dynamic content to load:
  waitFor: '.invoice-loaded',
  delay: 500,
  // Custom CSS for print:
  css: '@page { margin: 20mm; } .no-print { display: none; }',
});

Serving PDF as download in Express

import express from 'express';
import SnapSharp from '@snapsharp/sdk';

const app = express();
const client = new SnapSharp(process.env.SNAPSHARP_API_KEY);

app.get('/invoices/:id/pdf', async (req, res) => {
  const invoice = await getInvoice(req.params.id);

  // Option A: Screenshot the rendered invoice page
  const pdf = await client.screenshot({
    url: `${process.env.APP_URL}/invoices/${req.params.id}/render`,
    format: 'pdf',
    fullPage: true,
    waitFor: '.invoice-ready',
  });

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

Serving PDF in Next.js

// app/api/invoices/[id]/pdf/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import SnapSharp from '@snapsharp/sdk';

const client = new SnapSharp(process.env.SNAPSHARP_API_KEY!);

export async function GET(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  const { userId } = await auth();
  if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });

  const invoice = await getInvoice(params.id, userId);
  if (!invoice) return NextResponse.json({ error: 'Not found' }, { status: 404 });

  const pdf = await client.screenshot({
    url: `${process.env.NEXT_PUBLIC_APP_URL}/invoices/${params.id}/render?token=${invoice.printToken}`,
    format: 'pdf',
    fullPage: true,
    waitFor: '.invoice-ready',
  });

  return new NextResponse(pdf, {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename="invoice-${invoice.number}.pdf"`,
      'Cache-Control': 'private, no-store',
    },
  });
}

Async PDF for large documents

For long reports or PDFs with many pages, use async generation to avoid HTTP timeouts:

const job = await client.asyncScreenshot({
  url: 'https://yourapp.com/reports/annual-2025',
  format: 'pdf',
  fullPage: true,
  callbackUrl: 'https://yourapi.com/webhooks/pdf-ready',
});

// Webhook fires when PDF is ready
// payload: { jobId, status: 'completed', result: { url, size } }

Option 2: Puppeteer (self-hosted)

Use Puppeteer when you need full control, offline operation, or can't use external APIs:

import puppeteer from 'puppeteer';

async function generatePdf(url) {
  const browser = await puppeteer.launch({
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox'],
  });

  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'networkidle0' });

  const pdf = await page.pdf({
    format: 'A4',
    printBackground: true,
    margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
  });

  await browser.close();
  return pdf;
}

Tradeoffs vs. API:

  • You control the browser version and security settings
  • No per-request cost beyond your server
  • Requires 1–2GB RAM per browser instance
  • Needs browser management: pool, recycling, crash recovery
  • Cold start: 2–4s per new browser instance
  • Docker image: ~1.5GB with Chromium dependencies

Option 3: Server-side libraries (wkhtmltopdf, WeasyPrint)

Libraries like wkhtmltopdf (Node binding: html-pdf-node) or Python's WeasyPrint convert HTML to PDF without a full browser:

// html-pdf-node
import htmlPdfNode from 'html-pdf-node';

const file = { content: '<h1>Hello PDF</h1>' };
const options = { format: 'A4' };
const pdf = await htmlPdfNode.generatePdf(file, options);

Tradeoffs:

  • Faster than a full browser for simple layouts
  • Limited CSS support (no CSS Grid, flexbox is partial)
  • No JavaScript execution — dynamic content won't render
  • Binary dependency (wkhtmltopdf) requires system installation
  • Not maintained as actively as Playwright/Puppeteer

For invoices, contracts, or any page that uses modern CSS, a headless browser (Puppeteer or API) gives more reliable results.

When to use which approach

ScenarioRecommended
Invoice/receipt generation in SaaSScreenshot API
Report export for existing web pageScreenshot API
Offline operation, no external dependenciesPuppeteer
High volume (1,000+ PDFs/day) with budgetPuppeteer pool or API
Simple, static HTML, no CSS complexitywkhtmltopdf
CI/CD pipeline, periodic reportsScreenshot API
Microservice, no Node.js, any languageScreenshot API

Getting started with the API

# Get a free key at snapsharp.dev/sign-up
# Then:

curl "https://api.snapsharp.dev/v1/screenshot?url=https://example.com&format=pdf" \
  -H "Authorization: Bearer YOUR_KEY" \
  --output test.pdf

Free tier: 100 PDF requests/month. Requires Growth plan ($49/mo) for production use of the PDF endpoint.

See the full PDF documentation for all parameters including page size, margins, headers, and footers.

HTML to PDF API in JavaScript: Convert Web Pages to PDF Programmatically — SnapSharp Blog