How to auto-generate PDF invoices on Stripe payment
Source: Dev.to
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’ll have to generate them yourself.
Full pipeline: Stripe webhook → render HTML template → capture as PDF → email to customer.
Setup
You’ll need three things:
- A Stripe webhook that fires on
payment_intent.succeededorinvoice.payment_succeeded. - An HTML invoice template (see below).
- One PageBolt call to capture the rendered HTML as a PDF.
Install the required npm packages:
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 via PageBolt
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) => `
${line.description}
${line.currency} ${line.amount}
`
)
.join("");
return `
<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>
<div class="header">
<div class="logo">YourApp</div>
<div>
<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>Amount</th>
</tr>
</thead>
<tbody>
${lineItems}
</tbody>
</table>
<div class="total">Total: ${invoice.currency} ${invoice.total}</div>
`;
}
Also handle one‑time payments
For payment_intent.succeeded (non‑subscription payments) you can generate a simple receipt:
if (event.type === "payment_intent.succeeded") {
const paymentIntent = event.data.object;
// Retrieve the charge to get receipt details
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(),
// add any other fields you need
});
// …capture PDF and email as shown above
}
(Implement renderSimpleReceiptHtml similarly to the invoice template.)
Generate and email a PDF invoice (one‑time payments)
// Example continuation for one‑time payments
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 }),
});
const pdfBuffer = Buffer.from(await pdfRes.arrayBuffer());
// Email as shown in the invoice example
await sgMail.send({
to: charge.billing_details.email,
from: "billing@yourapp.com",
subject: "Payment receipt",
text: "Your payment was successful. Receipt attached.",
attachments: [
{
content: pdfBuffer.toString("base64"),
filename: "receipt.pdf",
type: "application/pdf",
disposition: "attachment",
},
],
});
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.
Try it free — 100 requests/month, no credit card required. → Get started in 2 minutes