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.
Option 1: Screenshot API (recommended)
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
| Scenario | Recommended |
|---|---|
| Invoice/receipt generation in SaaS | Screenshot API |
| Report export for existing web page | Screenshot API |
| Offline operation, no external dependencies | Puppeteer |
| High volume (1,000+ PDFs/day) with budget | Puppeteer pool or API |
| Simple, static HTML, no CSS complexity | wkhtmltopdf |
| CI/CD pipeline, periodic reports | Screenshot API |
| Microservice, no Node.js, any language | Screenshot 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.pdfFree 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.