How to Automate Invoice PDF Generation with an API
Stop exporting invoices by hand. Here's how to generate pixel-perfect branded PDF invoices automatically from your billing events.
Manually exporting invoices from a design tool is one of those tasks that feels fine when you have five customers and becomes a genuine problem when you have five hundred. This guide walks through building a fully automated invoice PDF pipeline — from a billing event in your backend to a branded PDF in your customer's inbox.
The problem with manual invoice exports
Most early-stage products start with a simple workflow: customer pays, you open Figma or Word, duplicate last month's invoice, change the date and amount, export as PDF, attach to an email. It works, but it doesn't scale — and it introduces consistency errors. Line items formatted differently between invoices, wrong dates, inconsistent rounding on totals, outdated logos after a rebrand.
The right solution is to treat invoices as code: a template that accepts data and produces a deterministic, pixel-perfect output every time.
Step 1: Design your invoice template
A Tailwind HTML template is the ideal format — it's readable, version-controlled alongside your application code, and renders faithfully to PDF via a real browser engine. Here's a minimal starting point:
const invoiceTemplate = (data: InvoiceData) => `
<div class="min-h-screen bg-white p-12 font-sans">
<!-- Header -->
<div class="flex items-start justify-between">
<div>
<h1 class="text-3xl font-bold text-zinc-900">INVOICE</h1>
<p class="mt-1 text-sm text-zinc-500">#${data.invoiceNumber}</p>
</div>
<div class="text-right text-sm text-zinc-600">
<p class="font-semibold text-zinc-900">${data.companyName}</p>
<p>${data.companyAddress}</p>
</div>
</div>
<!-- Bill to -->
<div class="mt-10">
<p class="text-xs font-semibold uppercase tracking-widest text-zinc-400">Bill to</p>
<p class="mt-1 font-semibold text-zinc-900">${data.clientName}</p>
<p class="text-sm text-zinc-500">${data.clientEmail}</p>
</div>
<!-- Line items -->
<table class="mt-10 w-full text-sm">
<thead>
<tr class="border-b border-zinc-200 text-left text-xs font-semibold uppercase tracking-widest text-zinc-400">
<th class="pb-3">Description</th>
<th class="pb-3 text-right">Amount</th>
</tr>
</thead>
<tbody>
${data.lineItems.map(item => `
<tr class="border-b border-zinc-100">
<td class="py-3 text-zinc-700">${item.description}</td>
<td class="py-3 text-right text-zinc-900">$${item.amount.toFixed(2)}</td>
</tr>
`).join('')}
</tbody>
</table>
<!-- Total -->
<div class="mt-6 flex justify-end">
<div class="text-right">
<p class="text-xs text-zinc-400">Total due</p>
<p class="text-2xl font-bold text-zinc-900">$${data.total.toFixed(2)}</p>
</div>
</div>
</div>
`;Step 2: Call the render API on your billing event
Wire the PDF generation into your payment webhook. When a payment succeeds, populate the template with the payment data and call the render endpoint:
async function generateInvoicePdf(data: InvoiceData): Promise<Buffer> {
const html = invoiceTemplate(data);
const response = await fetch('https://renderfy.io/api/v1/render', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.RENDERFY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'tailwind',
content: html,
options: { format: 'pdf', dimensions: 'a4' },
}),
});
if (!response.ok) throw new Error(`Render failed: ${response.status}`);
return Buffer.from(await response.arrayBuffer());
}Step 3: Store and deliver the PDF
Once you have the buffer, you have options:
- Upload to S3/R2/GCS: Store the PDF and include a download link in the confirmation email. The link is stable and the customer can access it from their billing history.
- Attach directly to the email: Use your transactional email provider's attachment API. Works well for invoices under ~5 MB.
- Stream on demand: Generate the PDF lazily when the customer clicks "Download invoice" — no storage required, always reflects the current template.
// Example: attach to email via Resend
await resend.emails.send({
from: '[email protected]',
to: customer.email,
subject: `Invoice #${invoice.number} — $${invoice.total}`,
html: '<p>Your invoice is attached.</p>',
attachments: [{
filename: `invoice-${invoice.number}.pdf`,
content: pdfBuffer,
}],
});Keeping templates in sync with your brand
Because the template is a string in your codebase, it gets updated in the same pull request as any other code change. Rebrand? Update the template, deploy, done — no invoices stuck with an old logo. The template is also testable: render it with fixture data in your test suite and compare against a reference PDF.
The end result is an invoice pipeline that runs without human involvement: payment event arrives → template populated → PDF rendered → attached to confirmation email → stored in billing history. The customer gets a professional, branded PDF and you never touch Figma for billing again.
Further reading
- How to Generate PDFs in Node.js Without Puppeteer — a comparison of every PDF generation approach and when to use each one
- Invoice Generator API — full reference for generating branded PDF invoices via API
- PDF Report API — generating scheduled reports, exports, and summaries as print-ready PDFs