Guides / Build an invoice
Build an invoice
This is a full template, start to finish: the data shape, the markup, and the call. It mirrors the Example invoice seeded into every new account, so you can follow along there.
1. Decide the data shape
Design the JSON your template will receive. A line-items array drives the table; a couple of nested objects hold the parties.
{
"number": "INV-2048",
"date": "18 June 2026",
"seller": { "name": "PDFglyph Ltd", "vatId": "FR12345678901" },
"customer": { "name": "Northwind Ltd", "address": "4 Trade St, London" },
"items": [
{ "description": "Consulting", "qty": 6, "unitPrice": "€150.00", "amount": "€900.00" },
{ "description": "Support", "qty": 1, "unitPrice": "€340.00", "amount": "€340.00" }
],
"subtotal": "€1,240.00",
"tax": "€248.00",
"total": "€1,488.00",
"notes": "Payment due within 30 days."
}Note that money is already formatted as strings. There are no formatting helpers in templates, so do currency, rounding, and dates in your code before sending. See formatting.
2. Write the template
Inline the CSS, set the page with @page, and pad the body since page margins are zero. The
line items come from {{#each items}}, and the notes block is conditional.
<!doctype html>
<html>
<head>
<style>
@page { size: A4; }
body { font-family: Inter, system-ui, sans-serif; color: #14322a; padding: 18mm; }
.head { display: flex; justify-content: space-between; align-items: flex-start; }
h1 { margin: 0; font-size: 28px; }
table { width: 100%; border-collapse: collapse; margin-top: 24px; }
th, td { text-align: left; padding: 8px 0; border-bottom: 1px solid #e3ece8; }
td.amt, th.amt { text-align: right; }
tr { break-inside: avoid; }
.totals { margin-top: 16px; width: 240px; margin-left: auto; }
.totals div { display: flex; justify-content: space-between; padding: 4px 0; }
.totals .grand { font-weight: 700; border-top: 2px solid #14322a; }
.notes { margin-top: 28px; color: #4b5b55; }
</style>
</head>
<body>
<div class="head">
<div>
<h1>Invoice {{number}}</h1>
<p>{{date}}</p>
</div>
<div>
<strong>{{seller.name}}</strong><br />
VAT {{seller.vatId}}
</div>
</div>
{{#with customer}}
<p>Billed to <strong>{{name}}</strong><br />{{address}}</p>
{{/with}}
<table>
<thead>
<tr>
<th>Description</th>
<th class="amt">Qty</th>
<th class="amt">Unit</th>
<th class="amt">Amount</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{description}}</td>
<td class="amt">{{qty}}</td>
<td class="amt">{{unitPrice}}</td>
<td class="amt">{{amount}}</td>
</tr>
{{/each}}
</tbody>
</table>
<div class="totals">
<div><span>Subtotal</span><span>{{subtotal}}</span></div>
<div><span>Tax</span><span>{{tax}}</span></div>
<div class="grand"><span>Total</span><span>{{total}}</span></div>
</div>
{{#if notes}}
<p class="notes">{{notes}}</p>
{{/if}}
</body>
</html>3. Preview and publish
Paste the template and the sample data into the editor, preview to confirm the layout, then publish. Publishing freezes this as version 1, which is what the API will render.
4. Generate
Post real data to the template's code. The response body is the invoice PDF.
curl -X POST https://api.pdfglyph.dev/v1/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateId": "tpl_invoice_8x2k",
"data": {
"number": "INV-2049",
"date": "19 June 2026",
"seller": {
"name": "PDFglyph Ltd",
"vatId": "FR12345678901"
},
"customer": {
"name": "Acme Co",
"address": "9 Market Sq, Berlin"
},
"items": [
{
"description": "Annual license",
"qty": 1,
"unitPrice": "€1,200.00",
"amount": "€1,200.00"
}
],
"subtotal": "€1,200.00",
"tax": "€240.00",
"total": "€1,440.00"
}
}' \
--output document.pdfThe notes field is omitted here, and because it is referenced inside {{#if notes}} it is
optional, so the call still succeeds.
Next
- More patterns in Common recipes.
- Tighten things up for real traffic in Going to production.