How to Generate PDF Invoices from HTML
Build an HTML invoice from validated data, style it for print, and render it to PDF with Puppeteer, Playwright, or WeasyPrint.
To generate a PDF invoice from HTML, populate an HTML template with validated invoice data, add print-specific CSS, then render the page with a PDF-capable browser such as Chromium through Puppeteer or Playwright. Wait for required data and assets, choose the paper size and margins deliberately, and inspect multi-page output. If the invoice does not need JavaScript, WeasyPrint is another option.
This guide covers a runnable Puppeteer implementation, Playwright and Python alternatives, print layout, asset loading, common failures, operational considerations, and an API option for capturing an invoice page that is already hosted.
1. Choose a renderer for the invoice
| Renderer | Choose it when | Relevant behavior |
|---|---|---|
| Puppeteer with Chromium | Your template uses modern browser layout or JavaScript, and a Node.js browser automation flow fits your deployment. | Page.pdf() uses print CSS media by default. Puppeteer documents that it waits for fonts by default. |
| Playwright with Chromium | Your project already uses Playwright or you need its documented PDF options. | page.pdf() returns a PDF buffer and uses print CSS media by default. Options include paper format and header/footer templates. The cited PDF export interface is Chromium-only. |
| WeasyPrint | The invoice is HTML and CSS and does not depend on browser-side JavaScript. | It documents PDF features such as hyperlinks, bookmarks, attachments, and forms. The project says it does not execute JavaScript. |
Compare the candidates on browser fidelity, JavaScript requirements, print CSS support, runtime and deployment footprint, asset loading, page-break behavior, output features, and maintenance. See the Puppeteer PDF guide, Playwright Page API, Playwright PDF export documentation, and the WeasyPrint project site for the documented constraints and features.
2. Build the invoice as data plus a template
Keep invoice calculations and source data separate from presentation. Validate the issuer, recipient, line items, currency, dates, and totals before rendering. The renderer should receive a complete invoice model; CSS should not determine financial values.
The following small template is embedded in the runnable Puppeteer example below. It uses semantic headings and a table, escapes dynamic text, prints a repeated table header, and requests that line-item rows avoid page splits where possible. Browser pagination behavior should still be checked in the renderer and version you deploy.
3. Add print CSS and define the page
Screen styles do not automatically make a reliable invoice PDF. Define print styles for the intended paper size, page margins, type size, table widths, colors, and page breaks. A4 and Letter are common choices, but choose the one appropriate to the recipient and workflow.
@page {
size: A4;
margin: 16mm 14mm 18mm;
}
@media print {
body { color: #111; background: #fff; font: 10pt/1.4 Arial, sans-serif; }
.screen-only { display: none !important; }
thead { display: table-header-group; }
tfoot { display: table-footer-group; }
tr, .keep-together { break-inside: avoid; }
h1, h2 { break-after: avoid; }
a { color: inherit; text-decoration: none; }
}
break-inside: avoid is a request to the renderer, not a universal guarantee. Very tall rows or blocks may still need to split. Check that headers repeat as intended, totals are not stranded, and no content is clipped. If you use backgrounds or brand colors, choose the PDF API’s background-print option where needed and verify the result; browser defaults may omit background graphics.
4. Generate a PDF with Puppeteer (Node.js)
Install Puppeteer in a Node.js project with npm install puppeteer. Save this as invoice.mjs and run node invoice.mjs. The example writes invoice.pdf in the current directory. It uses sample data; replace it with validated records from your application.
import puppeteer from 'puppeteer';
const invoice = {
number: 'INV-2026-0042',
issued: '2026-10-04',
due: '2026-10-18',
currency: 'USD',
seller: { name: 'Northstar Studio', email: 'billing@example.com' },
buyer: { name: 'Example Company', email: 'accounts@example.org' },
items: [
{ description: 'Interface design', quantity: 8, unitPrice: 12500 },
{ description: 'Implementation support', quantity: 3, unitPrice: 15000 }
]
};
function escapeHtml(value) {
return String(value).replace(/[<>&"']/g, ch => ({
'&': '&', '<': '<', '>': '>',
'"': '"', "'": '''
})[ch]);
}
function money(cents, currency) {
return new Intl.NumberFormat('en-US', {
style: 'currency', currency
}).format(cents / 100);
}
const subtotal = invoice.items.reduce(
(sum, item) => sum + item.quantity * item.unitPrice, 0
);
const rows = invoice.items.map(item => `
<tr>
<td>${escapeHtml(item.description)}</td>
<td class="number">${item.quantity}</td>
<td class="number">${money(item.unitPrice, invoice.currency)}</td>
<td class="number">${money(item.quantity * item.unitPrice, invoice.currency)}</td>
</tr>`).join('');
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Invoice ${escapeHtml(invoice.number)}</title>
<style>
@page { size: A4; margin: 16mm 14mm 18mm; }
* { box-sizing: border-box; }
body { color: #17202a; font: 10pt/1.45 Arial, sans-serif; }
h1 { margin: 0 0 18px; font-size: 24pt; }
.top, .parties { display: flex; justify-content: space-between; gap: 24px; }
.parties { margin: 28px 0; }
.label { color: #566573; font-size: 8pt; text-transform: uppercase; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px 6px; border-bottom: 1px solid #d5d8dc; text-align: left; }
th { background: #f2f4f4; }
thead { display: table-header-group; }
tr { break-inside: avoid; }
.number { text-align: right; white-space: nowrap; }
.total { margin: 18px 0 0 auto; width: 240px; text-align: right; font-weight: bold; }
@media print { a { color: inherit; text-decoration: none; } }
</style>
</head>
<body>
<div class="top">
<div><h1>Invoice</h1><strong>${escapeHtml(invoice.seller.name)}</strong><br>${escapeHtml(invoice.seller.email)}</div>
<div><div>Invoice number: ${escapeHtml(invoice.number)}</div><div>Issued: ${escapeHtml(invoice.issued)}</div><div>Due: ${escapeHtml(invoice.due)}</div></div>
</div>
<div class="parties">
<div><div class="label">Bill to</div>${escapeHtml(invoice.buyer.name)}<br>${escapeHtml(invoice.buyer.email)}</div>
</div>
<table>
<thead><tr><th>Description</th><th class="number">Qty</th><th class="number">Unit price</th><th class="number">Amount</th></tr></thead>
<tbody>${rows}</tbody>
</table>
<p class="total">Subtotal: ${money(subtotal, invoice.currency)}</p>
</body>
</html>`;
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
await page.evaluate(() => document.fonts.ready);
await page.pdf({
path: 'invoice.pdf',
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
displayHeaderFooter: false
});
} finally {
await browser.close();
}
The example uses integer minor units for line-item arithmetic to avoid accumulating ordinary binary floating-point fractions in the sample calculation. Adapt currency rounding, tax, discount, and jurisdiction-specific rules to your accounting requirements; the example is not a compliance specification. In production, validate currency codes and all numeric inputs before calling the formatter.
The template escapes text inserted into HTML. If invoice descriptions, customer names, or other values are untrusted, keep that escaping or use a trusted templating engine configured to escape HTML. Do not interpolate arbitrary customer content as executable markup or JavaScript.
5. Use Playwright instead
For a project using Playwright, the core flow is to load or set the invoice HTML, wait for required assets, and call page.pdf(). Install the package and Chromium browser using the Playwright installation instructions. This runnable sketch assumes the same html string from the preceding example:
import { chromium } from 'playwright';
import { writeFile } from 'node:fs/promises';
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle' });
await page.evaluate(() => document.fonts.ready);
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
displayHeaderFooter: false
});
await writeFile('invoice.pdf', pdf);
} finally {
await browser.close();
}
Playwright documents PDF paper formats such as Letter and A4 and header/footer templates. Its documented PDF export interface is Chromium-only, so use Chromium for this flow. Consult the Page API reference for the supported options in the version you deploy.
6. Use Python with Playwright
Install the Python package and Chromium browser with pip install playwright followed by playwright install chromium. Save a complete invoice document as invoice.html, then run this script:
import asyncio
from pathlib import Path
from playwright.async_api import async_playwright
async def main():
html = Path('invoice.html').read_text(encoding='utf-8')
async with async_playwright() as playwright:
browser = await playwright.chromium.launch()
try:
page = await browser.new_page()
await page.set_content(html, wait_until='networkidle')
await page.evaluate('document.fonts.ready')
pdf_bytes = await page.pdf(
format='A4',
print_background=True,
prefer_css_page_size=True,
display_header_footer=False,
)
Path('invoice.pdf').write_bytes(pdf_bytes)
finally:
await browser.close()
asyncio.run(main())
If the template depends on JavaScript to populate its invoice data, use page.goto() to load the application route and wait for an explicit application-ready selector before exporting. A network-idle condition alone does not prove that your data has rendered.
7. Use WeasyPrint for HTML and CSS without JavaScript
WeasyPrint can render HTML and CSS to PDF without launching a browser, but it does not execute JavaScript. Install it according to the official installation guide. A simple Python invocation for a saved, self-contained invoice document is:
from weasyprint import HTML
HTML(filename='invoice.html').write_pdf('invoice.pdf')
For external assets, ensure URLs resolve from the document base URL and that the rendering process can access them. Confirm the CSS and PDF features you rely on against the WeasyPrint version you deploy; consult its API reference.
8. Configure PDF options and page behavior
| Need | What to configure | What to verify |
|---|---|---|
| Paper dimensions | Use an explicit PDF format such as A4 or Letter, or a CSS @page size rule supported by the renderer. |
Check the resulting page dimensions and that the layout does not unexpectedly scale or clip. |
| Margins | Set margins in print CSS or the PDF API. Avoid setting conflicting values in both without checking precedence. | Ensure content, footer text, and totals fit inside the printable area. |
| Backgrounds | Enable background printing when the output needs CSS backgrounds or shaded table headings. | Inspect the PDF because default print settings may omit backgrounds. |
| Page breaks | Use print CSS break rules around sections and rows. | Test with enough rows to cross page boundaries, including unusually tall descriptions. |
| Repeated table headings | Use a table header group such as <thead> and check renderer behavior. |
Confirm headings repeat on continuation pages in the chosen version. |
| Headers and footers | Use renderer-supported header/footer templates if page numbers or fixed labels are needed. | Check overlap with page margins and ensure dynamic values render as expected. |
| Links and metadata | Choose a renderer whose PDF output supports the features required by your workflow. | Inspect links and document metadata in the saved file. |
Browser PDF APIs render using print media by default. Puppeteer documents that its PDF operation waits for fonts by default; still wait for application data and check remote images and other assets. See the Puppeteer guide and Playwright API.
9. Inspect edge cases before delivery
- Long invoices: render enough line items to create several pages. Check repeated headings, page breaks, totals placement, and rows that are taller than a page.
- Long text: use long product descriptions, addresses, and unbroken identifiers. Check wrapping and overflow.
- Unusual characters: check accented names, non-Latin scripts, symbols, and the chosen font’s glyph coverage.
- Amounts and rounding: verify subtotals, taxes, discounts, currency formatting, and rounding against the source accounting calculations.
- Missing assets: test with a remote image or font unavailable and decide whether generation should fail or use an approved fallback.
- Locale and dates: explicitly format dates, numbers, and currencies for the intended recipient rather than relying on a host machine’s locale.
- Security: escape untrusted HTML, restrict asset fetching when invoice data can control URLs, and avoid exposing internal services to a renderer that can navigate arbitrary addresses.
Invoice content and retention requirements depend on jurisdiction and business circumstances. Renderer documentation does not establish mandatory invoice fields, tax treatment, numbering, electronic invoicing rules, or retention periods. Check the relevant tax authority or qualified guidance for the applicable location before operational use.
10. Troubleshoot common PDF problems
| Symptom | Likely cause | Fix |
|---|---|---|
| PDF is blank or only partly rendered | The page was printed before client-side data or assets finished loading. | Wait for an application-ready selector or promise, then wait for fonts and required images. Do not treat a fixed delay as proof of readiness. |
| Styles look like the screen or differ from expectations | Print media rules, page size, or CSS support differ from the screen renderer. | Add and inspect @media print and @page rules. Check the selected renderer’s documented support. |
| Fonts are substituted or glyphs are missing | The font did not load, the PDF was created too early, or the font lacks those characters. | Wait for document.fonts.ready, confirm font URLs are reachable, and select a font with the needed glyph coverage. |
| Images are missing | Relative URLs have no usable base, remote fetches fail, or rendering starts early. | Use absolute URLs or a correct base URL, verify access from the rendering environment, and wait for image completion. |
| Background color or logo panel disappears | Background printing is disabled or a print style removes it. | Enable the PDF API’s background option and review print CSS. |
| Rows split awkwardly | Pagination rules are missing or a row is taller than available page space. | Apply break-avoid rules to practical row-sized content, shorten or split exceptional rows in the template, and inspect multi-page output. |
| PDF is clipped or unexpectedly scaled | Page dimensions and margins conflict, or content exceeds the printable width. | Set one intentional paper size and margin configuration; check wide tables and long unbroken strings. |
| Playwright PDF call fails in another browser | The documented PDF export interface is Chromium-only. | Launch Chromium for this flow or choose another renderer that supports the needed output. |
| WeasyPrint output misses interactive content | WeasyPrint does not execute JavaScript. | Pre-render the data into HTML, or use a browser renderer if client-side execution is required. |
| Invoice total differs from accounting system | Presentation code recalculated values with different rounding or tax rules. | Calculate and validate amounts in the authoritative business logic, then pass final values to the template. |
11. Performance, reliability, and cost
The research sources establish renderer behavior and options, but do not provide comparable performance benchmarks or hosting costs. Measure generation time, memory, and failure rates in the runtime and with the invoice sizes you expect rather than relying on a generic estimate.
- Reuse browser processes carefully: launching a browser for every invoice adds startup work; a managed browser process can serve multiple jobs, but isolate pages and always close them after use.
- Bound concurrency: rendering many large documents at once can consume substantial memory. Use a queue and a concurrency limit based on measured behavior.
- Set timeouts and cleanup: impose limits on navigation and asset loading, close pages in cleanup paths, and record enough error context to retry safely.
- Make retries idempotent: use a stable invoice identifier and avoid duplicate delivery or duplicate accounting side effects when a render job retries.
- Control external dependencies: remote font and image servers affect reliability. Prefer dependable asset locations, validate required assets, and define whether a missing optional asset blocks delivery.
- Protect invoice data: generated PDFs and temporary HTML may contain personal and financial information. Restrict access, avoid logging full documents or sensitive values, and apply the retention rules appropriate to your business.
Operational cost depends on your compute environment, browser runtime, volume, and maintenance needs. Compare total deployment and support costs for a browser process, a CSS-only renderer, or a hosted service with your own workload; no cost comparison is established by the cited renderer documentation.
12. Or skip the browser setup
If your invoice is already available at a URL, ScreenshotNeo can return a screenshot or PDF through one GET request. The API supports PDF output and PDF options such as paper size, margins, landscape, and page ranges. See the ScreenshotNeo API documentation for the current request parameters.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://your-site.example/invoices/INV-2026-0042 \
-d format=pdf \
-o invoice.pdf
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. Sign up for the free plan.
Frequently asked questions
Can I create a PDF without installing a browser?
Yes. WeasyPrint can render HTML and CSS without executing JavaScript. A hosted capture API is another option when the invoice page is reachable by URL.
Will the PDF include clickable links?
PDF link support depends on the renderer and how the document is authored. WeasyPrint documents hyperlinks among its PDF features; verify the output from your selected renderer.
Does generating the PDF make an invoice legally compliant?
No. PDF generation handles rendering. Required fields, tax treatment, numbering, and retention depend on the applicable jurisdiction and business circumstances.
Should I calculate taxes in the HTML template?
No. Keep authoritative financial calculations in business logic, validate them, and pass the resulting values into the presentation template.


