The journal · 14 July 2026 · 8 min read
How to generate PDF invoices from HTML in your app
You already have the invoice as HTML. The job is not designing a PDF, it is rendering the markup you have, at the right moment, and keeping the file you produced.
The short answer: render your existing HTML invoice template to PDF with a headless browser engine, triggered server side at the moment the invoice is finalized, and store the resulting file against the invoice record. Do not build the PDF by drawing boxes in code, and do not generate it in the customer's browser. The template you already have is the design; the only question is what renders it and who operates that renderer.
Almost every SaaS reaches this ticket eventually. Checkout works, billing works, and then someone in finance asks why customers cannot download a proper invoice. What follows is the version of this job that survives contact with production, written by people who render documents for a living.
Why you should render HTML, not build a PDF by hand
There are two schools. One draws the PDF programmatically: a library like FPDF, ReportLab or PDFKit where you position text at coordinates, draw a line here, set a font size there. The other renders an HTML template through a browser engine and prints the result.
Coordinate drawing is a trap for invoices specifically. An invoice is a table of unknown length. Add a line item and everything below it moves. Add a tax jurisdiction and a column appears. Localize into German and every label gets longer. In a coordinate based library, each of those is a code change with a pagination bug attached. In HTML, it is a table that reflows, because reflowing tables is the one thing browsers are unambiguously good at.
You almost certainly already have the invoice as HTML anyway, because you show it in the app. Rendering that same markup means the PDF and the screen cannot drift apart, and your designer can change the invoice without opening a Python file.
The four ways to turn that HTML into a PDF
| Approach | Where it runs | Good for | The catch |
|---|---|---|---|
| Browser print dialog | The customer's machine | Nothing, in a product | Output depends on their browser, their printer settings and their luck |
| Client side (jsPDF, html2canvas) | The customer's browser | Quick internal tools | Fuzzy text, no real pagination, and the invoice never exists on your servers |
| Self hosted headless Chrome | Your servers | High volume, if you want to own a browser fleet | Memory leaks, font packages, render drift, on call |
| Rendering API | Someone else's servers | Most teams | Per render cost, and your HTML leaves your network |
The client side option deserves a specific warning, because it keeps getting chosen for the wrong reason. Generating the invoice in the browser feels efficient: no server work, instant download. But it means the invoice is a screenshot of a DOM, rasterized, with text that may not be selectable and pagination that breaks the moment the item list runs past one page. Worse, the authoritative document now only ever existed on a laptop you do not control. When a customer disputes a charge in eighteen months, you have nothing to produce.
Invoices are financial records. Render them server side, where you can keep them.
What actually breaks when you render invoices
The rendering itself is easy. The failures are boring and specific, and they are all timing or typography.
The font falls back. Your invoice uses a web font from a CDN. The renderer starts printing before the font arrives, so the PDF ships in Times New Roman and finance thinks the invoice is fake. Fix: wait for the fonts to load, or embed them, and never trust a third party font host inside a render pipeline.
The total is missing. The line items come from an API call that resolves after first paint. The renderer prints an empty table. This is the single most common invoice PDF bug and it is why any serious renderer gives you an explicit wait condition. Tell it to wait for the element that contains the total, and it will not write the file until the number is on screen.
The table splits badly. A fourteen item invoice paginates so the last row lands alone on page two, above a subtotal that now appears orphaned from its own table. Print CSS fixes this: break-inside: avoid on rows, thead set to repeat, and a real page size rather than whatever the renderer defaults to.
The backgrounds vanish. Browsers strip background colors when printing unless told otherwise. Your neatly shaded header row prints white and the invoice looks broken. This is a one line fix and everybody hits it once.
None of these are hard problems. They are just problems you will meet in that order, in production, on a Friday. We wrote up the whole class of them in why HTML to PDF output does not match the browser.
The part almost nobody builds, and later wishes they had
Generating the invoice is half the job. The other half is being able to prove, later, what you sent.
Think about what a finance team or an auditor actually asks for. Not "can you regenerate invoice 8412", because a regenerated invoice reflects today's template, today's tax rates and today's company address. They want the document as it was issued, on the date it was issued. If your only artifact is a template plus a database row, you cannot produce that. You can produce a reconstruction, and a reconstruction is not a record.
So store the rendered PDF at the moment of issue, immutably, against the invoice. And if the invoice was rendered from a live page rather than a static template, keep a dated snapshot of that page too. This is exactly the gap timestamped archiving fills: the same render call that produces the customer's document also stores a capture of what was rendered, with its timestamp, retrievable years later. It costs nothing extra at generation time and it is impossible to backfill once the moment has passed.
The mirror image of this problem, incidentally, is what happens to the invoice after you send it. Your customer's accounts payable team receives a PDF and immediately needs the numbers out of it again, which is its own small industry: tools that extract line items and totals from an invoice PDF exist precisely because the document format that is best for humans is the worst for machines. Worth knowing if you are ever on the receiving end.
Doing it with an API
If you would rather not operate a browser fleet, the whole job collapses into one authenticated request: send the HTML, name the element to wait for, choose the page size, and ask for the render to be archived.
curl https://api.sitepdf.com/v1/render \
-H "Authorization: Bearer $SITEPDF_KEY" \
-F [email protected] \
-F format=Letter \
-F wait_for=".invoice-total" \
-F archive=true
What comes back is the PDF your customer downloads, plus an archive id and capture timestamp for the copy that stays with you. Wire that into the code path that marks an invoice as issued, store both references on the invoice row, and the feature is done. The PDF generator API page covers the full option set, and the free in-browser converter lets you check how your existing invoice template renders before you write any integration code at all.
One check worth doing before any of this: if your invoices already live in Stripe or QuickBooks, both hand you a finished PDF over their own API and you should fetch it rather than build a renderer. The invoice PDF API guide has those endpoints alongside the render path above, so you can tell in a minute which of the two jobs you actually have.
When you should self host instead
Honestly: when the numbers say so. If you are rendering tens of thousands of invoices a month, your templates are stable, and you already run browser automation for other reasons, a self hosted Puppeteer pool is a perfectly rational choice and the licence costs nothing. The cost lands as engineering time rather than an invoice line, which is exactly why teams underestimate it, but it is not infinite. Work it out honestly against the real operating burden of the fleet before you buy anything from anyone, including us.
A short checklist
- Render your existing HTML invoice template, server side, at the moment the invoice is issued.
- Wait for the total to be on screen before writing the file. This is the bug you will otherwise ship.
- Embed or preload fonts. Do not let a CDN decide what your invoice looks like.
- Set page size, margins and print CSS explicitly. Never accept renderer defaults for a financial document.
- Store the rendered PDF immutably against the invoice, and keep a dated capture of what was rendered.
- Do not generate financial documents in the customer's browser.
Get those six right and invoice PDFs stop being a recurring ticket and become a thing that quietly works.
One case worth calling out separately, because it comes up constantly: if the invoice data lives in a spreadsheet-style database rather than an application, the same principles apply but the tooling differs. Fixed-canvas layout builders break on the first invoice with an unusual number of line items, which is why generating invoice PDFs from Airtable ends up being an HTML templating problem too.
Written by the team building Sitepdf, an HTML to PDF API that archives every page it renders. The in-browser converter is free to try; early access locks the launch pricing.