This article is based on the Professional Invoice template from the Gallery.
The goal is simple: keep the invoice HTML stable, map variables clearly, and send a JSON payload that matches every placeholder.
Step 1 - Use a clear invoice HTML structure
Start with sections that mirror real invoice usage: header, parties, line items, totals, and footer notes.
HTML
<style>
body {
font-family: 'Segoe UI', sans-serif;
padding: 40px;
color: #1a1a2e;
background: #fff;
}
.header {
display: flex;
justify-content: space-between;
margin-bottom: 32px;
}
.parties {
display: flex;
justify-content: space-between;
margin-bottom: 24px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 10px 12px;
border-bottom: 1px solid #eceff4;
}
.text-right {
text-align: right;
}
.totals {
width: 280px;
margin-left: auto;
margin-top: 14px;
}
.totals-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
}
</style>
<div class="header">
<div>
<h1>{{company_name}}</h1>
<p>{{company_address}}</p>
</div>
<div class="text-right">
<h2>INVOICE</h2>
<p>Invoice #: {{invoice_number}}</p>
<p>Date: {{date}}</p>
<p>Due: {{due_date}}</p>
</div>
</div>
<div class="parties">
<div>
<h3>Bill To</h3>
<p>{{client_name}}</p>
<p>{{client_address}}</p>
<p>{{client_email}}</p>
</div>
<div>
<h3>Payment Info</h3>
<p>{{bank_name}}</p>
<p>{{iban}}</p>
</div>
</div>
<table>
<thead>
<tr>
<th>Description</th>
<th>Qty</th>
<th class="text-right">Unit Price</th>
<th class="text-right">Amount</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{this.description}}</td>
<td>{{this.qty}}</td>
<td class="text-right">${{this.unit_price}}</td>
<td class="text-right">${{this.amount}}</td>
</tr>
{{/each}}
</tbody>
</table>
<div class="totals">
<div class="totals-row"><span>Subtotal</span><span>${{subtotal}}</span></div>
<div class="totals-row"><span>Tax ({{tax_rate}}%)</span><span>${{tax_amount}}</span></div>
<div class="totals-row"><strong>Total</strong><strong>${{total}}</strong></div>
</div>
Step 2 - Send JSON that matches every variable path
JSON
{
"company_name": "Acme Corp",
"company_address": "123 Business Ave, Suite 100",
"invoice_number": "INV-2026-001",
"date": "March 9, 2026",
"due_date": "April 9, 2026",
"client_name": "John Smith",
"client_address": "456 Client St, City",
"client_email": "john@example.com",
"bank_name": "First National Bank",
"iban": "DE89 3704 0044 0532 0130 00",
"items": [
{
"description": "Web Development",
"qty": 40,
"unit_price": "150.00",
"amount": "6,000.00"
},
{
"description": "UI/UX Design",
"qty": 20,
"unit_price": "120.00",
"amount": "2,400.00"
}
],
"subtotal": "8,400.00",
"tax_rate": "18",
"tax_amount": "1,512.00",
"total": "9,912.00"
}
Step 3 - Final rendered preview in page
Rendered output preview
Template style preview
Preview uses the original Gallery template HTML and CSS with sample payload values.
Step 4 - Quick mapping check before render
{{company_name}}->company_name{{#each items}}->items[]{{this.qty}}->items[].qty{{tax_rate}}->tax_rate
If this map is consistent, render failures drop significantly.
Step 5 - Render through API
BASH
curl -X POST "https://api.hookpdf.com/v1/render" \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{
"template_id": "<professional_invoice_template_id>",
"payload": {
"company_name": "Acme Corp",
"invoice_number": "INV-2026-001",
"items": [{ "description": "Web Development", "qty": 40, "unit_price": "150.00", "amount": "6,000.00" }],
"subtotal": "6,000.00",
"tax_rate": "18",
"tax_amount": "1,080.00",
"total": "7,080.00"
}
}'
Use this template for free now
Start with this template in HookPDF and render your first PDF for free.
Use this template freeRelated reads
Ready to move from prototype to production?
Create your account and generate your first API-driven PDF with HookPDF.
Get your API key