Generating invoice PDFs from an API: the template, the layout traps, and the code

27 August 2026 · every rule below verified against a rendered file

Most guides on this stop at "post some HTML, get a PDF". Then your invoice reaches ten pages, the table headers disappear, a row splits across the page break, and the totals block ends up alone on a page of its own. This page is about that second half — with a full template you can copy, and each layout rule confirmed on a document we actually rendered.

First, decide which problem you have

"Invoice API" means two unrelated things, and picking the wrong one costs weeks.

If you need…You want
To charge customers, track what is owed, handle taxes and dunningA billing system — Stripe Invoicing, or your accounting software's API. It issues the PDF as a by-product.
A legally valid electronic invoice for a specific country (UBL, XRechnung, NF-e)A localised e-invoicing provider. The XML is the document; the PDF is decoration.
To turn data you already have into a document that looks the way you wantA rendering API. That is what this page covers.

If your invoice already exists in your own system and you just need it as a file — with your layout, your fonts, your logo — you are in the third row.

The request

curl -X POST https://emitforge.com/v1/pdf \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "<html>…your invoice…</html>",
    "data": { "invoice": { "number": "2026-0142" } },
    "format": "A4",
    "margin": "18mm"
  }'

Back comes a signed URL to the file, its size, and how much it counted against your allowance.

Building the line items

Templates here substitute values; they do not loop. That is deliberate — a template engine that evaluates expressions is a remote-code-execution surface, and templates arrive from outside. So the rows get built where you already have a loop, in your own code:

const esc = s => String(s).replace(/[&<>"']/g, c =>
  ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));

const rows_html = items.map(i => `
  <tr>
    <td class="desc">${esc(i.description)}<small>${esc(i.detail)}</small></td>
    <td class="num">${i.qty}</td>
    <td class="num">${money(i.unit)}</td>
    <td class="num">${money(i.qty * i.unit)}</td>
  </tr>`).join('');

Then inject with triple braces — {{{rows_html}}} — which do not escape. Single braces escape by default, so a customer called <script> renders as text. The triple form exists so that the moment you take responsibility for escaping is a visible, deliberate one.

The five rules that decide whether a long invoice is usable

We rendered a 60-line invoice into a five-page PDF and checked every page. These are the rules that mattered.

1. Repeat the table header on every page

thead { display: table-header-group; }

Without it, pages two onward show columns with no names. This is the single most common complaint about generated invoices, and it is one line.

2. Never split a row

tr { break-inside: avoid; }

3. Keep the totals block together

.totals { break-inside: avoid; }

Nothing looks more broken than "Subtotal" at the bottom of page four and "Total due" alone at the top of page five.

4. Embed the font, never link it

The rendering machine does not have your local fonts. And a font from a CDN makes every render wait on the network — that wait counts against the timeout, and one bad day at the CDN turns into wrong-looking invoices. Embed as a data URI, or use a system stack and accept it.

5. Fixed footers do not work the way you expect

This one cost us a rendered file to discover. The advice that circulates is position: fixed with a negative offset to pin a footer to every page. In headless Chromium it does not stay at the foot of the printed page — it reappears inside the content and runs straight through the first row of every subsequent page. We saw it happen, in the PDF, over the line items.

Repeating footers and "page X of Y" live in @page margin boxes, which headless print does not expose reliably. The stable options: pass the page count in your payload and write it into the text, or accept a footer only at the end.

A complete template

Copy this and change the styling. It is the one we tested — header, both parties, item table with repeating header, totals that stay together, and a payment block.

<style>
  @page { size: A4; margin: 18mm 16mm 22mm 16mm; }
  body { font-family: Georgia, serif; color:#1f1c19; font-size:12.5px; margin:0 }

  table.items { width:100%; border-collapse:collapse }
  table.items thead { display: table-header-group }   /* rule 1 */
  table.items tr    { break-inside: avoid }           /* rule 2 */
  table.items th { text-align:left; font-size:8.5px; letter-spacing:.12em;
                   text-transform:uppercase; color:#8a8079;
                   border-bottom:1px solid #d8d2ca; padding:0 0 6px }
  table.items td { padding:8px 0; border-bottom:1px solid #f2eee9 }
  .num { text-align:right; white-space:nowrap }

  .totals { margin:20px 0 0 auto; width:54%; break-inside: avoid }   /* rule 3 */
  .totals .grand td { border-top:2px solid #1f1c19; font-size:16px; font-weight:bold }
</style>

<table class="items">
  <thead>
    <tr><th>Description</th><th class="num">Qty</th>
        <th class="num">Unit</th><th class="num">Amount</th></tr>
  </thead>
  <tbody>{{{rows_html}}}</tbody>
</table>

<table class="totals">
  <tr><td>Subtotal</td><td class="num">{{totals.subtotal}}</td></tr>
  <tr><td>{{totals.tax_label}}</td><td class="num">{{totals.tax}}</td></tr>
  <tr class="grand"><td>Total due</td><td class="num">{{totals.total}}</td></tr>
</table>

The full version, with header, addresses and payment block, is in this file — free to copy whether or not you use our API. It is HTML and CSS; it renders the same in any Chromium-based renderer, including a self-hosted Gotenberg.

Answers to what people ask

How do I generate a PDF invoice?

Build the HTML with your data — either in your own code or by substituting values into a template — and send it to a renderer that turns HTML into PDF. The layout rules above are what separate a one-page demo from an invoice that survives fifty line items.

Is there a free API that can create PDFs?

Yes, in two senses. Self-hosting Gotenberg is free and open source, and you pay in operations rather than money. And most hosted APIs, ours included, have a free tier — ours is 100 renders a month with no card.

How do I generate an API for e-invoicing?

That is a different problem from rendering. Legal e-invoicing means producing a structured document in a national format — UBL, XRechnung, FatturaPA, NF-e — usually transmitted through an accredited channel. The PDF is the human-readable companion, not the legal document. Use a localised e-invoicing provider for that, and a rendering API only for the visual part.

How do I get the PDF invoice back to my user?

Our response gives you a signed URL valid for 24 hours, which is normally what you want: it is small, it does not hold a connection open, and it drops straight into an email or storage step. If you need the bytes, fetch that URL from your own code and store or stream them.

If you want the rendering part handled

emitforge takes the template and the data and returns the file. Real Chromium, so the CSS above behaves exactly as it did in your browser. 100 renders a month free, no card. Published limits: 10-second timeout, 2 MB template, 10 requests per second, files deleted after 24 hours.

Get a key   Read the docs

Straight with you: new service, one person, one endpoint. No template editor, no asynchronous rendering, no SLA, no signed DPA. If you are issuing invoices at volume in a regulated business, an established vendor with those in writing is the better call — and the template above works with them too.

Related guides