接入 Stripe 支付
配置 Stripe 产品、价格和 Webhook,连接购买流程与权益发放。
上一篇已经定义套餐和权益,并实现使用限制。这一篇配置 Stripe,让购买、支付通知和权益发放形成完整流程。
接入支付系统
我们已经在前面完成 products.ts 中的产品配置,现在需要接入真实的支付系统。
这里以 Stripe 为例,假设你已经完成注册并开通了收款权限。第一次接入时,建议先在测试模式下跑通 Checkout 和 Webhook,再切换到正式模式。两个环境各自使用一套商品、Price ID 和密钥,不能混用。
创建产品
接下来可以让 AI 创建网站的 Logo 图片。我一般直接把商品图片设置为网站 Logo;如果不想这样做,也可以根据 products.ts 中的产品配置单独生成商品图片。
然后再让 AI 根据配置生成对应的名称和描述,方便在 Stripe 中添加产品。例如下面是我的提示词:
我要将产品发布到 Stripe 上,你帮我生成对应的 name 和 description,积分包也要有。
AI 给我生成了下面的表格:
| 产品 | Name | Description |
|---|---|---|
| Pro 会员 | Webpage to PDF Pro | Get 60 minutes of webpage conversion time every day across Quick Convert, Custom, and Visual Editor, plus 500 API conversion credits each month. Failed conversions do not use your allowance. |
| 500 积分包 | Webpage to PDF API Credits — 500 | 500 prepaid credits for successful API conversions. One successful conversion uses one credit. Purchased credits do not expire, and failed conversions do not use credits. |
| 2,000 积分包 | Webpage to PDF API Credits — 2,000 | 2,000 prepaid credits for successful API conversions. One successful conversion uses one credit. Purchased credits do not expire, and failed conversions do not use credits. |
| 10,000 积分包 | Webpage to PDF API Credits — 10,000 | 10,000 prepaid credits for successful API conversions. One successful conversion uses one credit. Purchased credits do not expire, and failed conversions do not use credits. |
虽然首版不会在网站上展示积分包,但可以先在 Stripe 中把这些商品建好,等 API 功能开放后再启用对应套餐。
打开 Stripe 的 Product catalog 页面,点击右上角的 Create product,将上面的表格数据填入对应输入框,然后点击 Add product 完成创建。

创建完成后的产品列表如下:

接下来就是将产品和积分包对应的 Price ID 配置到 config/products.ts 中,我们这里拿 Pro 会员为例,点击刚刚创建的 Pro 会员产品,在打开的页面中可以看到该产品的 Pricing 列表:

你可以点击这些列表项查看这些 Pricing 项目的详情,又或者打开右侧的操作菜单,在下拉列表中选择 Copy price ID 复制对应的 Price ID。

按照上面的步骤,补全 config/products.ts 中的产品配置,核心是替换配置中的 priceId 字段,注意每个套餐配置应填写对应的 Price ID,Pro 月付和年付分别使用各自的 Price ID,不要混用,否则会导致支付系统无法正确扣费。
配置 Webhook
Stripe 提供了 Webhook 功能,可以让我们在用户支付成功后收到通知,我们根据这些通知来更新数据库中的订单状态,以及对应的权益和额度。
设置 Webhook 的步骤很简单。首先在 Stripe 后台页面的下方点击 Developers 菜单:

选择 Webhooks 面板:

点击页面中间的 Add destination,进入 Create an event destination 页面:

接下来的操作就是选择当前网站关注的事件列表,目前的 Saavo 项目中关注的事件列表如下:
checkout.session.completed
checkout.session.async_payment_succeeded
checkout.session.async_payment_failed
checkout.session.expired
customer.subscription.created
customer.subscription.updated
customer.subscription.paused
customer.subscription.resumed
customer.subscription.deleted
invoice.finalized
invoice.finalization_failed
invoice.paid
invoice.payment_failed
invoice.payment_action_required
invoice.marked_uncollectible
invoice.voided
refund.created
refund.updated
refund.failed一共 19 个事件:

点击 Continue,在新页面中选择 Webhook endpoint 类型。确认后按页面提示填写表单并完成创建。

复制密钥到本地
创建好 Webhook 后,Stripe 会生成一个 Webhook Secret,需要将它保存到本地环境变量中。该密钥以 whsec_ 开头。

复制后保存到项目根目录的 .env.production 文件中:
STRIPE_WEBHOOK_SECRET="whsec_xxxxxxxxxxxxxxxxxxxxxx"如果没找到 .env.production 文件,可以创建一个,不影响后续使用。
除了 Webhook 密钥,还需要将 Stripe 的连接 ID 和 Secret Key 配置到本地环境变量中。
STRIPE_CONNECTION_ID="webpage_to_pdf"
STRIPE_SECRET_KEY="sk_live_xxxxxxxxxxxxxxxxxxxxxx"其中,STRIPE_SECRET_KEY 可以在 Stripe 后台找到,正式密钥以 sk_live_ 开头。

STRIPE_CONNECTION_ID 是自己定义的连接 ID,用来区分不同的支付连接。
使用脚本快速设置
前面演示的是手动创建方式,Webhook 也可以使用项目脚本快速设置。这个脚本不会替你申请 Stripe 密钥,只是代替“配置 Webhook”小节中的后台操作。
使用前先运行 npm run,确认项目包含这些命令。旧模板可能需要同时更新脚本入口和实现,参见项目命令。
生产环境执行 npm run webhook:stripe:prod,开发环境执行 npm run webhook:stripe:dev。下面演示的是开发环境的设置过程。
执行前,需要先配置对应环境的 STRIPE_SECRET_KEY,否则脚本会报错:
C:\code\webpagetopdf>npm run webhook:stripe:dev
> webpagetopdf@0.0.1 webhook:stripe:dev
> tsx scripts/stripe/setup.ts development
Webhook site origin [http://127.0.0.1:5173]: https://7c7d-2605-52c0-2-1ebf-be24-11ff-fe62-cadd.ngrok-free.app
✔ Select the webhook event API version 2026-08-26.dahlia (project default)
✔ Select Stripe events checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, checkout.session.expired, customer.subscription.created, customer.subscription.updated, customer.subscription.paused, customer.subscription.resumed, customer.subscription.deleted, invoice.finalized, invoice.finalization_failed, invoice.paid, invoice.payment_failed, invoice.payment_action_required, invoice.marked_uncollectible, invoice.voided, refund.created, refund.updated, refund.failed
Environment: development
Destination: https://7c7d-2605-52c0-2-1ebf-be24-11ff-fe62-cadd.ngrok-free.app/api/webhooks/stripe
Webhook API version: 2026-08-26.dahlia
Selected events: 19
Create this Stripe webhook destination? [Y/n]:
Stripe webhook destination created: we_1UEnlFLG8YPa5bzGMCs1REYC
Configuration file: C:\code\webpagetopdf\.env
The new signing secret was saved without being printed.
Restart the development server if the signing secret changed.完善支付逻辑
Saavo 已经把支付相关的底层逻辑处理好了,这里不需要再自己调用 Stripe SDK。我们只需要根据 products.ts 中配置的产品和套餐,把 Pricing 页面和购买入口接上。
整体流程大概是这样:
- Pricing 页面读取产品和套餐配置。用户点击购买后,将
productId和planId提交到/api/payments/checkout/:productId/:planId。 - 后端根据这两个参数重新读取套餐配置,检查用户是否登录、套餐是否存在、是否允许重复购买,然后使用套餐中的
priceId创建 Stripe Checkout Session。 - 接口返回 Stripe Checkout 地址,前端跳转过去完成付款。
- Stripe 在支付、退款、订阅状态变化时,会向
/api/webhooks/stripe发送 Webhook。后端使用STRIPE_WEBHOOK_SECRET验证签名,再更新订单、交易、订阅和退款记录。 - 系统根据 Stripe 返回的 Price ID 找到
products.ts中对应的套餐,再读取access.roles和access.entitlements。 - 一次性付款成功后会触发
payment_succeeded。订阅创建、更新或终止时,也会触发对应事件,角色和权益会跟着自动更新。
有一点需要注意,不能根据前端是否跳转成功来判断付款结果。用户可能中途关闭页面,也可以自己构造请求,最终的支付状态要以 Stripe Webhook 为准。
Saavo 已经处理了 Webhook 持久化和重复事件。某次处理失败时,接口会返回失败状态,Stripe 后面还会继续重试。我们真正需要改的主要还是前端 UI,包括套餐展示、调用 Checkout 接口、处理登录状态和接口错误等等。
订单、订阅、角色和权益这些数据都交给后端 Webhook 更新,前端不要自己改,否则会导致数据不一致。
简单来说,可以把 Pricing 页面和购买入口交给 AI 完成,让它按照当前的 products.ts 配置处理即可。
本篇检查
完成这一篇后,应在测试模式下走通购买、Webhook 回调和权益发放,再为正式环境准备对应的配置。