Generate Invoice PDFs from HTML Templates
Invoices are one of the most common PDF use cases for SaaS applications. Instead of wrestling with PDF libraries, you can design your invoice in HTML and CSS, then convert it to a pixel-perfect PDF with an API. This guide walks through building a professional invoice PDF generator with a reusable HTML template.
#Why use HTML for invoice templates?
HTML and CSS give you full control over your invoice design without learning a PDF-specific library. Benefits include:
- Familiar tooling: Use the same HTML/CSS skills you already have
- Easy iteration: Preview in a browser, then convert to PDF
- Dynamic content: Inject data with template literals or a templating engine
- Consistent branding: Use your existing stylesheets and web fonts
The challenge is ensuring the PDF output looks exactly like your browser preview. A PDF API handles this by rendering your HTML in a real browser engine.
#Structuring the invoice HTML
A professional invoice has four sections: a header with company info, client details, a line-items table, and totals. Here is the basic document skeleton:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>/* styles go here */</style>
</head>
<body>
<div class="invoice-header"><!-- company + invoice # --></div>
<div class="addresses"><!-- bill-to address --></div>
<table class="items-table"><!-- line items --></table>
<div class="totals"><!-- subtotal, tax, total --></div>
<div class="footer"><!-- thank-you note --></div>
</body>
</html>
#Invoice header
The header shows your company name and the invoice number, date, and due date side by side:
<div class="invoice-header">
<div class="company-info">
<h1>Acme Inc.</h1>
<p>123 Business Street</p>
<p>San Francisco, CA 94102</p>
<p>billing@acme.com</p>
</div>
<div class="invoice-details">
<h2>INVOICE</h2>
<p class="invoice-number">#INV-2024-001</p>
<p>Date: January 15, 2024</p>
<p>Due: February 15, 2024</p>
</div>
</div>
#Line-items table
Use a standard HTML table for line items. Tables render consistently across PDF engines and handle page breaks well:
<table class="items-table">
<thead>
<tr>
<th>Description</th>
<th>Qty</th>
<th>Rate</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="item-name">Website Development</div>
<div class="item-desc">Custom landing page</div>
</td>
<td>1</td>
<td>$2,500.00</td>
<td>$2,500.00</td>
</tr>
</tbody>
</table>
#Totals and footer
Keep the totals block right-aligned and visually separated from the table:
<div class="totals">
<div class="totals-row subtotal">
<span>Subtotal</span>
<span>$3,847.00</span>
</div>
<div class="totals-row">
<span>Tax (10%)</span>
<span>$384.70</span>
</div>
<div class="totals-row total">
<span>Total Due</span>
<span>$4,231.70</span>
</div>
</div>
#Styling the invoice with CSS
Use print-friendly CSS: no viewport units, explicit colors, and simple layouts. Here are the key styles:
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #333;
padding: 40px;
}
.invoice-header {
display: flex;
justify-content: space-between;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #eee;
}
.company-info h1 {
font-size: 28px;
color: #2563eb;
margin-bottom: 8px;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
}
.items-table th {
background: #f8f9fa;
padding: 12px;
text-align: left;
font-size: 12px;
text-transform: uppercase;
color: #666;
border-bottom: 2px solid #eee;
}
.items-table td {
padding: 16px 12px;
border-bottom: 1px solid #eee;
}
.totals {
width: 300px;
margin-left: auto;
}
.totals-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
}
.totals-row.total {
font-size: 18px;
font-weight: bold;
border-top: 2px solid #333;
margin-top: 8px;
padding-top: 12px;
}
Print-friendly CSS
Avoid transparency, viewport units, and complex animations. Stick to explicit colors, px/pt/mm units, and simple flexbox layouts for reliable PDF rendering.
#Making the template dynamic
A static template is not useful for real invoices. Create a function that populates the template with data from your database:
function generateInvoiceHtml(invoice) {
const fmt = (amount) =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: invoice.currency || 'USD',
}).format(amount);
const fmtDate = (d) =>
new Date(d).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const rows = invoice.items
.map(
(item) => `<tr>
<td>${item.name}</td>
<td>${item.quantity}</td>
<td>${fmt(item.rate)}</td>
<td>${fmt(item.quantity * item.rate)}</td>
</tr>`
)
.join('');
const subtotal = invoice.items.reduce(
(sum, i) => sum + i.quantity * i.rate,
0
);
const tax = subtotal * (invoice.taxRate || 0);
const total = subtotal + tax;
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>/* your CSS here */</style>
</head>
<body>
<div class="invoice-header">
<div class="company-info">
<h1>${invoice.company.name}</h1>
<p>${invoice.company.address}</p>
<p>${invoice.company.email}</p>
</div>
<div class="invoice-details">
<h2>INVOICE</h2>
<p>#${invoice.number}</p>
<p>Date: ${fmtDate(invoice.date)}</p>
<p>Due: ${fmtDate(invoice.dueDate)}</p>
</div>
</div>
<table class="items-table">
<thead>
<tr>
<th>Description</th>
<th>Qty</th>
<th>Rate</th>
<th>Amount</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
<div class="totals">
<div class="totals-row">
<span>Subtotal</span>
<span>${fmt(subtotal)}</span>
</div>
<div class="totals-row">
<span>Tax</span>
<span>${fmt(tax)}</span>
</div>
<div class="totals-row total">
<span>Total Due</span>
<span>${fmt(total)}</span>
</div>
</div>
</body>
</html>`;
}
#Converting to PDF with PDFLoom API
Send the generated HTML to the PDFLoom API to get a downloadable PDF:
async function createInvoicePdf(invoice) {
const html = generateInvoiceHtml(invoice);
const response = await fetch(
'https://api.pdfloom.com/v1/convert/html',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PDFLOOM_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: html,
options: {
format: 'A4',
printBackground: true,
margin: {
top: '10mm',
right: '10mm',
bottom: '10mm',
left: '10mm',
},
},
}),
}
);
if (!response.ok) {
throw new Error(`PDF generation failed: ${response.statusText}`);
}
const data = await response.json();
return data.url;
}
API response
The API returns a signed S3 URL that is valid for 30 minutes. Download the PDF immediately, email it to your customer, or store it in your own storage.
#Example usage
const invoice = {
number: 'INV-2024-001',
date: '2024-01-15',
dueDate: '2024-02-15',
currency: 'USD',
taxRate: 0.1,
company: {
name: 'Acme Inc.',
address: '123 Business Street, San Francisco, CA',
email: 'billing@acme.com',
},
client: {
name: 'John Smith',
company: 'Smith Enterprises LLC',
email: 'john@smithenterprises.com',
},
items: [
{ name: 'Website Development', quantity: 1, rate: 2500 },
{ name: 'API Integration', quantity: 8, rate: 150 },
{ name: 'Monthly Hosting', quantity: 3, rate: 49 },
],
};
createInvoicePdf(invoice)
.then((url) => console.log('Invoice PDF:', url))
.catch((err) => console.error('Error:', err));
#Integrating with an Express.js API
Here is how you might wire up invoice PDF generation as an API endpoint:
app.post('/api/invoices/:id/pdf', async (req, res) => {
try {
const invoice = await db.invoices.findById(req.params.id);
if (!invoice) {
return res.status(404).json({ error: 'Invoice not found' });
}
const pdfUrl = await createInvoicePdf(invoice);
res.json({ url: pdfUrl });
} catch (error) {
console.error('PDF generation error:', error);
res.status(500).json({ error: 'Failed to generate PDF' });
}
});
#Advanced customizations
#Adding a company logo
Embed logos as base64 data URLs for faster rendering, or reference a publicly accessible URL:
<img
src="data:image/png;base64,iVBORw0KGgo..."
alt="Company Logo"
style="height: 50px;"
/>
Image loading
If using external URLs, make sure your images are publicly accessible. The PDF renderer fetches them during conversion. Base64 embedding avoids this issue entirely.
#Custom fonts
Load Google Fonts directly in your HTML. The PDF renderer will fetch them during conversion:
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700"
rel="stylesheet"
/>
<style>
body { font-family: 'Inter', sans-serif; }
</style>
#Page break control
For invoices with many line items, control where pages break with print-specific CSS:
.items-table tr { page-break-inside: avoid; }
.totals { page-break-inside: avoid; }
.footer { page-break-before: avoid; }
#Best practices for invoice PDFs
- Use a consistent invoice number format (e.g., INV-2024-001) for easy tracking
- Include all legally required information for your jurisdiction
- Keep the design clean and readable — invoices need to be functional, not flashy
- Test with different data lengths — ensure long company names or many line items do not break the layout
- Store a copy of the generated PDF for your records, do not rely on regenerating from data
#Next steps
You now have a complete invoice PDF generator. To extend this further:
- Add support for different invoice templates (standard, detailed, proforma)
- Implement PDF storage and retrieval
- Set up automated invoice emails with the PDF attached
- Add support for multiple currencies and locales
Ready to start generating invoices? Create a free PDFLoom account and get 50 credits to test your implementation. Check the API documentation for additional options like headers, footers, and page numbers.
Related posts
PDF Generation in Serverless Environments
Learn how to generate PDFs in serverless environments like Vercel, AWS Lambda, and Netlify. Includes working code examples and a comparison of approaches.
Convert Any URL to PDF with a Single API Call
Learn how to convert any public URL to a PDF or screenshot using the PDFLoom API. Includes code examples in cURL, Node.js, and a full Express.js integration.
Puppeteer PDF: Common Problems and How to Fix Them
Troubleshoot Puppeteer PDF issues: timeouts, memory leaks, Docker problems, and font rendering. Working code examples and solutions.