Renderpaper
Log in Start free

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:

  1. A line-item table that comes from data, not from markup.
  2. Totals that are computed before rendering, not by the template.
  3. Sensible behaviour when the table runs past one page.
  4. 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.

Last updated 2026-08-08.