Back to Blog
Guide February 26, 2026 · 6 min read

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:

  1. A Stripe webhook that fires on payment_intent.succeeded or invoice.payment_succeeded
  2. An HTML invoice template
  3. 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 →