如何在 Stripe 支付时自动生成 PDF 发票
发布: (2026年2月26日 GMT+8 16:17)
5 分钟阅读
原文: Dev.to
Source: Dev.to
Stripe 自动生成收据,但它们带有 Stripe 品牌且无法自定义。如果您需要与品牌匹配的发票、包含自定义项目,或在某些国家出于增值税合规的要求,您必须自行生成发票。
完整流程: Stripe webhook → 渲染 HTML 模板 → 捕获为 PDF → 发送电子邮件给客户。
设置
您需要三样东西:
- Stripe webhook,在
payment_intent.succeeded或invoice.payment_succeeded触发。 - HTML 发票模板(见下文)。
- 一次 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 分钟快速入门