How to Auto-Generate PDF Invoices on Stripe Payment
Stripe generates receipts automatically, but they're Stripe-branded and can't be customized. Here's the full pipeline: webhook → render HTML template → capture as PDF → email to customer.
Stripe generates receipts automatically, but they're Stripe-branded and can't be customized. If you need invoices that match your brand, include custom line items, or are required for VAT compliance in certain countries, you need to generate them yourself.
Here's the full pipeline: Stripe webhook → render HTML template → capture as PDF → email to customer.
The setup
Three things needed:
- A Stripe webhook that fires on
payment_intent.succeededorinvoice.payment_succeeded - An HTML invoice template
- One PageBolt call to capture it as PDF
npm install stripe @sendgrid/mail express
Webhook handler
import Stripe from "stripe";
import sgMail from "@sendgrid/mail";
import express from "express";
import { renderInvoiceHtml } from "./templates/invoice.js";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const app = express();
// Use raw body for Stripe signature verification
app.post(
"/webhooks/stripe",
express.raw({ type: "application/json" }),
async (req, res) => {
const sig = req.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
if (event.type === "invoice.payment_succeeded") {
await handleInvoicePaid(event.data.object);
}
res.json({ received: true });
}
);
Generate and send the PDF
async function handleInvoicePaid(stripeInvoice) {
// Fetch full customer details
const customer = await stripe.customers.retrieve(stripeInvoice.customer);
// Build your invoice data
const invoiceData = {
number: stripeInvoice.number,
date: new Date(stripeInvoice.created * 1000).toLocaleDateString(),
customerName: customer.name,
customerEmail: customer.email,
lines: stripeInvoice.lines.data.map((line) => ({
description: line.description,
amount: (line.amount / 100).toFixed(2),
currency: stripeInvoice.currency.toUpperCase(),
})),
total: (stripeInvoice.amount_paid / 100).toFixed(2),
currency: stripeInvoice.currency.toUpperCase(),
};
// Render HTML template
const html = renderInvoiceHtml(invoiceData);
// Capture as PDF
const pdfRes = await fetch("https://pagebolt.dev/api/v1/pdf", {
method: "POST",
headers: {
"x-api-key": process.env.PAGEBOLT_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ html }),
});
const pdfBuffer = Buffer.from(await pdfRes.arrayBuffer());
// Email to customer
await sgMail.send({
to: customer.email,
from: "billing@yourapp.com",
subject: `Invoice ${invoiceData.number} — Payment confirmed`,
text: `Hi ${customer.name}, your payment was successful. Invoice attached.`,
attachments: [
{
content: pdfBuffer.toString("base64"),
filename: `invoice-${invoiceData.number}.pdf`,
type: "application/pdf",
disposition: "attachment",
},
],
});
console.log(`Invoice ${invoiceData.number} sent to ${customer.email}`);
}
HTML invoice template
// templates/invoice.js
export function renderInvoiceHtml(invoice) {
const lineItems = invoice.lines
.map(
(line) => `
<tr>
<td>${line.description}</td>
<td style="text-align:right">${line.currency} ${line.amount}</td>
</tr>`
)
.join("");
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: -apple-system, sans-serif; color: #111; max-width: 680px; margin: 40px auto; padding: 0 20px; }
.header { display: flex; justify-content: space-between; margin-bottom: 40px; }
.logo { font-size: 24px; font-weight: 700; }
table { width: 100%; border-collapse: collapse; margin: 24px 0; }
th { text-align: left; border-bottom: 2px solid #111; padding: 8px 0; }
td { padding: 10px 0; border-bottom: 1px solid #eee; }
.total { font-size: 18px; font-weight: 700; text-align: right; margin-top: 16px; }
.meta { color: #666; font-size: 14px; }
</style>
</head>
<body>
<div class="header">
<div class="logo">YourApp</div>
<div class="meta">
<div>Invoice ${invoice.number}</div>
<div>${invoice.date}</div>
</div>
</div>
<div>
<strong>Bill to:</strong><br>
${invoice.customerName}<br>
${invoice.customerEmail}
</div>
<table>
<thead><tr><th>Description</th><th style="text-align:right">Amount</th></tr></thead>
<tbody>${lineItems}</tbody>
</table>
<div class="total">Total: ${invoice.currency} ${invoice.total}</div>
</body>
</html>`;
}
Also handle one-time payments
For payment_intent.succeeded (non-subscription payments):
if (event.type === "payment_intent.succeeded") {
const paymentIntent = event.data.object;
const charges = await stripe.charges.list({
payment_intent: paymentIntent.id,
limit: 1,
});
const charge = charges.data[0];
if (!charge?.billing_details?.email) return;
const html = renderSimpleReceiptHtml({
amount: (paymentIntent.amount / 100).toFixed(2),
currency: paymentIntent.currency.toUpperCase(),
email: charge.billing_details.email,
date: new Date().toLocaleDateString(),
description: paymentIntent.description || "Payment",
});
const pdfRes = await fetch("https://pagebolt.dev/api/v1/pdf", {
method: "POST",
headers: { "x-api-key": process.env.PAGEBOLT_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ html }),
});
// ... email as above
}
Store PDFs in S3 (optional)
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "us-east-1" });
async function storePdf(pdfBuffer, invoiceNumber) {
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: `invoices/${invoiceNumber}.pdf`,
Body: pdfBuffer,
ContentType: "application/pdf",
}));
return `https://${process.env.S3_BUCKET}.s3.amazonaws.com/invoices/${invoiceNumber}.pdf`;
}
Test locally with Stripe CLI
# Forward webhooks to your local server
stripe listen --forward-to localhost:3000/webhooks/stripe
# Trigger a test payment
stripe trigger invoice.payment_succeeded
The PDF will be generated and emailed within seconds of the test event firing.
Get Started Free
100 requests/month, no credit card
Generate branded PDF invoices automatically on every Stripe payment — render from HTML, email to customer, store in S3.
Get Your Free API Key →