Every SaaS app eventually needs to send invoices. And every time, developers reach for a PDF library — pdfkit, puppeteer, jsPDF — and spend days wiring it up.
There's a better way. Here's how to auto-generate a pixel-perfect PDF invoice from a Stripe webhook in under 20 lines of code.
The Problem with PDF Libraries
If you've ever used pdfkit or puppeteer directly in production, you know the pain:
- You bundle a headless browser or a heavy library into your app
- You write 200+ lines of layout code for a simple invoice
- It breaks on every Node.js version upgrade
- You deal with font rendering, page breaks, and margins manually
For a SaaS app, this is a distraction from your core product.
The Setup: Stripe Webhook + RenderPDFs
Here's the full flow:
- Customer pays → Stripe fires
invoice.payment_succeeded - Your webhook handler builds an HTML invoice template
- You call RenderPDFs API → get back a PDF URL
- You email the PDF link to your customer
Let's build it.
Step 1: The Invoice HTML Template
Design your invoice in plain HTML and CSS — no special PDF syntax needed:
function buildInvoiceHTML(invoice) {
return `
<html>
<head>
<style>
body { font-family: Arial, sans-serif; padding: 40px; color: #333; }
.header { display: flex; justify-content: space-between; margin-bottom: 40px; }
.company { font-size: 24px; font-weight: bold; color: #6d28d9; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th { background: #f3f4f6; padding: 12px; text-align: left; }
td { padding: 12px; border-bottom: 1px solid #e5e7eb; }
.total { font-size: 18px; font-weight: bold; text-align: right; margin-top: 20px; }
</style>
</head>
<body>
<div class="header">
<div class="company">Acme SaaS</div>
<div>Invoice #${invoice.number}<br>${new Date(invoice.created * 1000).toLocaleDateString()}</div>
</div>
<p>Bill to: <strong>${invoice.customer_email}</strong></p>
<table>
<tr><th>Description</th><th>Amount</th></tr>
${invoice.lines.data.map(line => `
<tr><td>${line.description}</td><td>$${(line.amount / 100).toFixed(2)}</td></tr>
`).join("")}
</table>
<div class="total">Total: $${(invoice.amount_paid / 100).toFixed(2)}</div>
</body>
</html>
`;
}
Step 2: Call RenderPDFs from Your Stripe Webhook
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function POST(req) {
const sig = req.headers["stripe-signature"];
const event = stripe.webhooks.constructEvent(
await req.text(),
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
if (event.type === "invoice.payment_succeeded") {
const invoice = event.data.object;
// Build HTML invoice
const html = buildInvoiceHTML(invoice);
// Generate PDF with RenderPDFs
const response = await fetch("https://api.renderpdfs.com/v1/pdf/html", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.RENDERPDFS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: { format: "A4", printBackground: true },
}),
});
const { url } = await response.json();
// Email the PDF link to your customer
await sendEmail({
to: invoice.customer_email,
subject: `Your invoice #${invoice.number}`,
body: `Your invoice is ready: ${url}`,
});
}
return new Response("ok");
}
That's it. No library to maintain. No Puppeteer in your bundle. No PDF layout headaches.
Why This Works Better
| Approach | Bundle size | Setup time | Maintenance |
|---|---|---|---|
| pdfkit | +15MB | 2–3 days | High |
| Puppeteer in-app | +300MB | 3–5 days | Very high |
| RenderPDFs API | 0MB | 30 min | None |
The PDF URL returned has a 24-hour expiry — perfect for emailing customers without storing files yourself.
Get Started
- Sign up at renderpdfs.com — free tier included
- Grab your API key from the dashboard
- Add
RENDERPDFS_API_KEYto your environment variables - Copy the webhook code above and adapt your HTML template
First PDF in under 60 seconds. No credit card required.
Have questions about the implementation? Drop them in the comments — happy to help! 🚀
Top comments (0)