Invoice PDF Generation from HTML
Invoices are layout-sensitive. A total split from its line items, a missing page, or a signature on the wrong page can delay payment. This guide covers a production invoice pipeline from HTML.
1. Build the invoice HTML
Use a semantic table for line items, a totals block, payment terms, and a footer. Keep each row atomic so it never splits across pages.
tr, .totals, .signature, .payment-terms {
break-inside: avoid;
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
2. Generate the invoice
Save the invoice layout as a template and pass the invoice data. Format currency and dates in your backend before passing strings to the template.
const invoice = await client.generate({
templateId: 'YOUR_INVOICE_TEMPLATE_ID',
data: {
invoiceNumber: 'INV-2026-1042',
customer: 'Acme Ltd',
issueDate: '2026-10-30',
dueDate: '2026-11-29',
currency: 'USD',
items: [
{ description: 'Design retainer', quantity: 1, amount: '$840.00' },
{ description: 'Support', quantity: 2, amount: '$400.00' }
],
subtotal: '$1,240.00',
tax: '$0.00',
total: '$1,240.00'
},
downloadBuffer: true
});
Keep the expanded template under the 20 MB request limit. See Limits & file sizes.
3. Keep line items and totals together
Compare the last line item's page with the totals block. If they differ, return a compact patch or fail the request with a clear reason.
const rows = doc.querySelectorAll('table.invoice tbody tr');
const totals = doc.querySelector('.totals');
if (!rows.length || !totals) {
return { success: false, reason: 'Missing line items or totals' };
}
const lastRowPage = rows[rows.length - 1].pages[0];
if (lastRowPage !== totals.pages[0]) {
return {
success: false,
patch: { compact: true },
reason: 'Totals split from line items'
};
}
return { success: true, rows: rows.length };
4. Handle multi-page invoices
Repeat table headers with thead { display: table-header-group; }, add page numbers, and keep totals on the final page. Use a testCase to assert the row count and total.
const rows = doc.querySelectorAll('table.invoice tbody tr');
const total = doc.querySelector('#invoice-total');
return {
success: Boolean(rows.length && total),
rowCount: rows.length,
total: total ? total.innerText.trim() : null
};
5. Signatures and payment details
Query the signature block after layout and return its page and bounding box. Keep it on the same page as the payment terms and final total.
const signature = doc.querySelector('#signature');
const terms = doc.querySelector('.payment-terms');
return {
success: Boolean(signature && terms && signature.pages[0] === terms.pages[0]),
signaturePage: signature ? signature.pages[0] + 1 : null
};
See Extract element coordinates for e-signatures.
6. Currency, tax, and locale
Format amounts in your backend with Intl.NumberFormat and pass strings to the template. Use fonts that cover the currency symbol and locale, and avoid floating-point rounding inside the template.
const format = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
const total = format.format(1240); // '$1,240.00'
7. Validate every invoice
Assert the row count, total, page count, and signature presence. Return custom keys in testResults and fail CI or the delivery job when an assertion breaks.
8. Production checklist
- Use a stable invoice number and idempotency key.
- Store the generated PDF and its metadata.
- Use webhooks for batch invoicing.
- Test long descriptions, large totals, and multiple currencies.
- Check the request, PDF, and
testResultslimits.
Generate a sample invoice
Start with the generation options and add a layout check.