Recipe
Invoice template
A complete Handlebars invoice template: line items, per-rate tax, totals, payment terms, and a table head that repeats on every page.
Worth knowing
- The totals block is wrapped in break-inside: avoid so an invoice never splits between the last line and the amount due. A reader who sees only one of those halves has to go looking for the other.
- Amounts arrive as major units, because formatCurrency formats them: 4308.00, not 430800. Keep integers in your own database and convert on the way out.
- thead { display: table-header-group } is what makes page two of a long invoice legible. Without it the columns are unlabelled.
The template
Store it once with POST /v1/templates. It is compiled when you store it, so a mistake surfaces now rather than on the first render.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Noto Sans', sans-serif;
font-size: 10pt;
line-height: 1.5;
color: #111827;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
table { border-collapse: collapse; width: 100%; }
thead { display: table-header-group; }
tr { break-inside: avoid; }
.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.band { background: {{brand_color}}; color: #fff; padding: 9mm 15mm; }
.band h1 { margin: 0; font-size: 24pt; letter-spacing: -0.01em; }
.band .number { margin-top: 2mm; opacity: 0.85; }
.sheet { padding: 0 15mm; }
.parties { display: flex; gap: 10mm; margin-top: 10mm; }
.party { flex: 1; }
.party h2 {
margin: 0 0 2mm;
font-size: 7.5pt;
letter-spacing: 0.14em;
text-transform: uppercase;
color: {{brand_color}};
}
.party .name { font-weight: 700; font-size: 11pt; }
.party .address { color: #4b5563; font-size: 9pt; white-space: pre-line; }
.items { margin-top: 11mm; }
.items th {
padding: 2.5mm 3mm;
background: #f3f4f6;
font-size: 8pt;
letter-spacing: 0.08em;
text-transform: uppercase;
text-align: left;
}
.items td { padding: 3mm; border-bottom: 1px solid #f0f1f3; }
.totals { break-inside: avoid; margin-top: 7mm; margin-left: auto; width: 80mm; }
.totals th { padding: 1.8mm 4mm 1.8mm 0; font-weight: 400; color: #6b7280; text-align: left; }
.totals .due th, .totals .due td {
padding-top: 3mm;
border-top: 1.5px solid #111827;
font-size: 12pt;
font-weight: 700;
}
.closing { break-inside: avoid; margin-top: 12mm; color: #4b5563; font-size: 9pt; }
.closing h2 {
font-size: 7.5pt;
letter-spacing: 0.14em;
text-transform: uppercase;
color: {{brand_color}};
margin: 0 0 1.5mm;
}
</style>
</head>
<body>
<header class="band">
<h1>{{title}}</h1>
<div class="number">{{number}}</div>
</header>
<div class="sheet">
<section class="parties">
<div class="party">
<h2>From</h2>
<div class="name">{{seller.name}}</div>
<div class="address">{{seller.address}}</div>
{{#if seller.tax_id}}<div class="address">VAT {{seller.tax_id}}</div>{{/if}}
</div>
<div class="party">
<h2>Bill to</h2>
<div class="name">{{buyer.name}}</div>
<div class="address">{{buyer.address}}</div>
</div>
<div class="party">
<h2>Details</h2>
<div class="address">Issued {{formatDate issued_on locale=locale}}</div>
{{#if due_on}}<div class="address">Due {{formatDate due_on locale=locale}}</div>{{/if}}
</div>
</section>
<table class="items">
<thead>
<tr>
<th>Description</th>
<th class="num">Qty</th>
<th class="num">Unit price</th>
<th class="num">Amount</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{this.description}}</td>
<td class="num">{{formatNumber this.quantity maximumFractionDigits=3}}</td>
<td class="num">{{formatCurrency this.unit_price currency=../currency locale=../locale}}</td>
<td class="num">{{formatCurrency this.amount currency=../currency locale=../locale}}</td>
</tr>
{{/each}}
</tbody>
</table>
<table class="totals">
<tr>
<th>Subtotal</th>
<td class="num">{{formatCurrency subtotal currency=currency locale=locale}}</td>
</tr>
{{#if discount}}
<tr>
<th>Discount</th>
<td class="num">-{{formatCurrency discount currency=currency locale=locale}}</td>
</tr>
{{/if}}
{{#each tax_rows}}
<tr>
<th>{{this.label}}</th>
<td class="num">{{formatCurrency this.amount currency=../currency locale=../locale}}</td>
</tr>
{{/each}}
<tr class="due">
<th>Total</th>
<td class="num">{{formatCurrency total currency=currency locale=locale}}</td>
</tr>
</table>
{{#if payment_terms}}
<section class="closing">
<h2>Payment terms</h2>
<div>{{payment_terms}}</div>
</section>
{{/if}}
</div>
</body>
</html>Rendering it
Substitute the template id you got back, and send this payload.
curl https://api.paperpony.dev/v1/pdf/render \
-H "Authorization: Bearer $PAPERPONY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template_id": "tpl_YOUR_TEMPLATE_ID",
"data": {
"title": "Invoice",
"number": "INV-0001",
"issued_on": "2026-07-26",
"due_on": "2026-08-25",
"currency": "USD",
"locale": "en-US",
"brand_color": "#1f2937",
"seller": {
"name": "Northwind Studio",
"address": "14 Bridgewater Street\nManchester M15 4RN\nUnited Kingdom",
"tax_id": "GB123456789"
},
"buyer": {
"name": "Fabrikam Ltd",
"address": "9 Peveril Road\nSheffield S11 7AQ"
},
"items": [
{
"description": "Brand identity, discovery",
"quantity": 12,
"unit_price": 95,
"amount": 1140
},
{
"description": "Logo design",
"quantity": 1,
"unit_price": 1800,
"amount": 1800
},
{
"description": "Brand guidelines",
"quantity": 1,
"unit_price": 650,
"amount": 650
}
],
"subtotal": 3590,
"discount": 0,
"tax_rows": [
{
"label": "VAT 20%",
"amount": 718
}
],
"total": 4308,
"payment_terms": "Payment due within 30 days. Bank transfer to Northwind Studio."
},
"options": {
"format": "A4",
"margin": {
"top": "0",
"right": "0",
"bottom": "16mm",
"left": "0"
},
"footer_html": "<div style=\"width:100%;padding:0 15mm;font-family:Helvetica,sans-serif;font-size:7pt;color:#9ca3af;text-align:right\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>"
}
}'import { PaperPony } from 'paperpony';
const pony = new PaperPony({ apiKey: process.env.PAPERPONY_API_KEY });
const job = await pony.pdf.render({
"template_id": "tpl_YOUR_TEMPLATE_ID",
"data": {
"title": "Invoice",
"number": "INV-0001",
"issued_on": "2026-07-26",
"due_on": "2026-08-25",
"currency": "USD",
"locale": "en-US",
"brand_color": "#1f2937",
"seller": {
"name": "Northwind Studio",
"address": "14 Bridgewater Street\nManchester M15 4RN\nUnited Kingdom",
"tax_id": "GB123456789"
},
"buyer": {
"name": "Fabrikam Ltd",
"address": "9 Peveril Road\nSheffield S11 7AQ"
},
"items": [
{
"description": "Brand identity, discovery",
"quantity": 12,
"unit_price": 95,
"amount": 1140
},
{
"description": "Logo design",
"quantity": 1,
"unit_price": 1800,
"amount": 1800
},
{
"description": "Brand guidelines",
"quantity": 1,
"unit_price": 650,
"amount": 650
}
],
"subtotal": 3590,
"discount": 0,
"tax_rows": [
{
"label": "VAT 20%",
"amount": 718
}
],
"total": 4308,
"payment_terms": "Payment due within 30 days. Bank transfer to Northwind Studio."
},
"options": {
"format": "A4",
"margin": {
"top": "0",
"right": "0",
"bottom": "16mm",
"left": "0"
},
"footer_html": "<div style=\"width:100%;padding:0 15mm;font-family:Helvetica,sans-serif;font-size:7pt;color:#9ca3af;text-align:right\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>"
}
});
console.log(job.output_url);import os
import requests
response = requests.post(
"https://api.paperpony.dev/v1/pdf/render",
headers={"Authorization": f"Bearer {os.environ['PAPERPONY_API_KEY']}"},
json={
"template_id": "tpl_YOUR_TEMPLATE_ID",
"data": {
"title": "Invoice",
"number": "INV-0001",
"issued_on": "2026-07-26",
"due_on": "2026-08-25",
"currency": "USD",
"locale": "en-US",
"brand_color": "#1f2937",
"seller": {
"name": "Northwind Studio",
"address": "14 Bridgewater Street\nManchester M15 4RN\nUnited Kingdom",
"tax_id": "GB123456789"
},
"buyer": {
"name": "Fabrikam Ltd",
"address": "9 Peveril Road\nSheffield S11 7AQ"
},
"items": [
{
"description": "Brand identity, discovery",
"quantity": 12,
"unit_price": 95,
"amount": 1140
},
{
"description": "Logo design",
"quantity": 1,
"unit_price": 1800,
"amount": 1800
},
{
"description": "Brand guidelines",
"quantity": 1,
"unit_price": 650,
"amount": 650
}
],
"subtotal": 3590,
"discount": 0,
"tax_rows": [
{
"label": "VAT 20%",
"amount": 718
}
],
"total": 4308,
"payment_terms": "Payment due within 30 days. Bank transfer to Northwind Studio."
},
"options": {
"format": "A4",
"margin": {
"top": "0",
"right": "0",
"bottom": "16mm",
"left": "0"
},
"footer_html": "<div style=\"width:100%;padding:0 15mm;font-family:Helvetica,sans-serif;font-size:7pt;color:#9ca3af;text-align:right\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>"
}
},
)
response.raise_for_status()
print(response.json()["output_url"])