Generate an invoice PDF from HTML
An invoice is the document most people are trying to make when they search for HTML to PDF, and it has four requirements that a "hello world" render does not:
- A line-item table that comes from data, not from markup.
- Totals that are computed before rendering, not by the template.
- Sensible behaviour when the table runs past one page.
- A header that repeats on page two, so the second sheet is identifiably part of the same invoice.
This page covers all four. The technique is the same regardless of language — the
examples use curl so nothing is hidden behind a client library.
The template
Store this once. The data comes later, so the same template serves every customer.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
@page { size: A4; margin: 18mm 16mm; }
body { font: 13px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif; color: #16181d; }
h1 { font-size: 26px; margin: 0 0 2px; letter-spacing: -.3px; }
.meta { color: #5b6270; margin: 0 0 22px; }
.parties { display: flex; gap: 48px; margin-bottom: 26px; }
.party h2 { font-size: 10px; text-transform: uppercase; letter-spacing: .08em;
color: #5b6270; margin: 0 0 4px; }
.party p { margin: 0; }
table { width: 100%; border-collapse: collapse; }
th { font-size: 10px; text-transform: uppercase; letter-spacing: .08em;
color: #5b6270; text-align: left; padding: 0 0 6px; }
td { padding: 7px 0; border-bottom: 1px solid #e6e8ee; vertical-align: top; }
.num { text-align: right; white-space: nowrap; }
/* Repeat the header row on every page the table spills onto, and never split a
line item across the page boundary. */
thead { display: table-header-group; }
tr { break-inside: avoid; }
.totals { margin-top: 18px; margin-left: auto; width: 46%; }
.totals td { border: none; padding: 3px 0; }
.totals .grand td { border-top: 2px solid #16181d; padding-top: 8px;
font-weight: 700; font-size: 15px; }
.terms { margin-top: 34px; color: #5b6270; font-size: 11.5px; }
</style>
</head>
<body>
<h1>Invoice {{.InvoiceNumber}}</h1>
<p class="meta">Issued {{.IssueDate}} · Due {{.DueDate}}</p>
<div class="parties">
<div class="party">
<h2>Billed to</h2>
<p>{{.Customer.Name}}</p>
<p>{{.Customer.Address}}</p>
<p>{{.Customer.City}}</p>
</div>
<div class="party">
<h2>From</h2>
<p>{{.Vendor.Name}}</p>
<p>{{.Vendor.Address}}</p>
<p>{{.Vendor.City}}</p>
</div>
</div>
<table>
<thead>
<tr>
<th>Description</th>
<th class="num">Qty</th>
<th class="num">Unit</th>
<th class="num">Amount</th>
</tr>
</thead>
<tbody>
{{range .LineItems}}
<tr>
<td>{{.Description}}</td>
<td class="num">{{.Quantity}}</td>
<td class="num">{{.UnitPrice}}</td>
<td class="num">{{.Amount}}</td>
</tr>
{{end}}
</tbody>
</table>
<table class="totals">
<tr><td>Subtotal</td><td class="num">{{.Subtotal}}</td></tr>
<tr><td>VAT {{.VatRate}}</td><td class="num">{{.Vat}}</td></tr>
<tr class="grand"><td>Total due</td><td class="num">{{.Total}}</td></tr>
</table>
<p class="terms">
Payment within {{.PaymentDays}} days to {{.Vendor.IBAN}}. Reference {{.InvoiceNumber}}.
</p>
</body>
</html>
Rendering it with data
Store the template — through the API or the editor — then post JSON:
curl -X POST https://renderpaper.com/v1/templates/your-template-id/render \
-H "X-API-Key: $RENDER_API_KEY" \
-H "Content-Type: application/json" \
-o invoice.pdf \
-d '{
"data": {
"InvoiceNumber": "2026-0142",
"IssueDate": "2026-08-08",
"DueDate": "2026-09-07",
"Customer": { "Name": "Northwind Trading AB", "Address": "Sveavägen 12", "City": "111 57 Stockholm" },
"Vendor": { "Name": "Your Company AB", "Address": "1 Example Street", "City": "111 22 Stockholm",
"IBAN": "SE00 0000 0000 0000 0000 0000" },
"LineItems": [
{ "Description": "Consulting, July", "Quantity": 12, "UnitPrice": "1 200.00", "Amount": "14 400.00" },
{ "Description": "Hosting, July", "Quantity": 1, "UnitPrice": "4 350.00", "Amount": "4 350.00" },
{ "Description": "Support retainer", "Quantity": 1, "UnitPrice": "2 500.00", "Amount": "2 500.00" }
],
"Subtotal": "21 250.00",
"VatRate": "25%",
"Vat": "5 312.50",
"Total": "26 562.50",
"PaymentDays": 30
}
}'
Format money before you send it
Notice the amounts above are strings, already formatted. That is deliberate.
Go templates have no currency formatting, and no rounding rules that match your
accounting. If you send 26562.5 you will render 26562.5. Worse, if you send a
float and let anything downstream round it, you have a document that disagrees with
your ledger by a cent — in a legal record.
Format in your application, where the locale and the rounding rules already live, and send strings. The template's job is layout.
What happens when the table is long
Two CSS declarations do the work, and both are in the template above:
thead { display: table-header-group; }
tr { break-inside: avoid; }
table-header-group tells the browser to repeat <thead> at the top of every page
the table continues onto. break-inside: avoid on the row stops a single line item
being cut in half by the page boundary.
This is also the answer to a repeating document header — not just a table
header. Chromium's headerTemplate renders in an isolated context that cannot see
your stylesheet, which is why so many people end up with an unstyled header or none
at all. Renderpaper has no header option for exactly that reason. Wrapping the
document in an outer table with a thead puts the repeated content in normal flow,
where your CSS applies and where it pushes content down instead of painting over
it. That technique is written up in full, with measurements, in
the repeating-header recipe.
Page size and margins
@page { size: A4; margin: 18mm 16mm; } in the document wins over any option sent
with the request. That is deliberate — the document decides its own geometry, so a
template designed for A4 does not silently become Letter because of a default
somewhere else.
For US Letter, change the template:
@page { size: Letter; margin: 0.75in 0.6in; }
Try it without a key
https://renderpaper.com/sample.pdf is a rendered invoice, publicly served, no
account. The free tier is 50 documents a month with no card.