DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to auto-generate PDF invoices on Stripe payment

How to Auto-Generate PDF Invoices on Stripe Payment

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
Enter fullscreen mode Exit fullscreen mode

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 });
  }
);
Enter fullscreen mode Exit fullscreen mode

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}`);
}
Enter fullscreen mode Exit fullscreen mode

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>`;
}
Enter fullscreen mode Exit fullscreen mode

Also handle one-time payments

For payment_intent.succeeded (non-subscription payments):

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(),
    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
}
Enter fullscreen mode Exit fullscreen mode

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`;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The PDF will be generated and emailed within seconds of the test event firing.


Try it free — 100 requests/month, no credit card. → Get started in 2 minutes

Top comments (0)