All posts
·5 min read·By Marija Renderfy

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.

PDFAutomationTutorial

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

Try it yourself

100 free credits. No card needed. First render in under 5 minutes.

Get started free