如何在 Stripe 支付时自动生成 PDF 发票

发布: (2026年2月26日 GMT+8 16:17)
5 分钟阅读
原文: Dev.to

Source: Dev.to

Stripe 自动生成收据,但它们带有 Stripe 品牌且无法自定义。如果您需要与品牌匹配的发票、包含自定义项目,或在某些国家出于增值税合规的要求,您必须自行生成发票。

完整流程: Stripe webhook → 渲染 HTML 模板 → 捕获为 PDF → 发送电子邮件给客户。

设置

您需要三样东西:

  1. Stripe webhook,在 payment_intent.succeededinvoice.payment_succeeded 触发。
  2. HTML 发票模板(见下文)。
  3. 一次 PageBolt 调用,将渲染后的 HTML 捕获为 PDF。

安装所需的 npm 包:

npm install stripe @sendgrid/mail express

Webhook 处理程序

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();

// 使用原始请求体进行 Stripe 签名验证
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 });
  }
);

生成并发送 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 发票模板

// 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>
  `;
}

处理一次性付款

对于 payment_intent.succeeded(非订阅付款),您可以生成一张简易收据:

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
}

实现 renderSimpleReceiptHtml 时可参考发票模板。

生成并通过电子邮件发送 PDF 发票(一次性付款)

// 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",
    },
  ],
});

将 PDF 存储到 S3(可选)

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`;
}

本地使用 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

PDF 将在测试事件触发后几秒钟内生成并通过电子邮件发送。

免费试用 — 每月 100 次请求,无需信用卡。 → 2 分钟快速入门

0 浏览
Back to Blog

相关文章

阅读更多 »