Waffo Pancake
如何设置和使用 Waffo Pancake 处理支付和订阅
MkSaaS 支持使用 Waffo Pancake 处理一次性支付和订阅 支付。Waffo 是一家 Merchant of Record(MoR,记录商), 可以代您处理全球税务计算、代收、合规和结算。
该集成使用官方 @waffo/pancake-ts SDK, 并使用 Waffo 托管的结账页面和消费者门户。卡信息不会经过您的应用服务器。
设置
MkSaaS 默认包含免费计划、月度和年度专业版订阅计划,以及终身一次性支付计划。
创建 Waffo 账户
在 pancake.waffo.ai 注册 Waffo 账户,并在引导过程中创建商店。基础设置可以参考 Waffo 快速入门。
获取 API 凭据
在 Waffo 控制台打开 API & Development,点击 Create API Key,复制
Merchant ID 和私钥,并将它们保存为 WAFFO_MERCHANT_ID 和
WAFFO_PRIVATE_KEY。
WAFFO_MERCHANT_ID 是 Merchant ID,不是 storeId,也不是从 URL 中复制
的商店标识。API 密钥在创建时绑定测试或生产环境,因此请为每个环境创建
独立的密钥。
私钥只能存放在服务端。将 PEM 私钥保存到环境变量时,使用转义的 \n 保留
换行:
WAFFO_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----"配置 Webhook
在 Waffo 控制台 > Settings > Webhooks 中点击 Add Webhook,并配置:
- 负载格式:Raw。只有这种格式会携带验签所需的
X-Waffo-Signature请求头。 - URL:
https://YOUR-DOMAIN.com/api/webhooks/waffo - 事件:保留Webhook 事件一节列出的支付、订阅和退款事件。
Waffo 不需要 Webhook 签名密钥环境变量。@waffo/pancake-ts SDK 内置验证
公钥,并通过 verifyWebhook() 验证原始请求体。
创建产品
Waffo 结账使用以 PROD_ 开头的 Product ID,而不是 Stripe 风格的价格 ID:
- 创建月度 Subscription 产品,将产品 ID 保存为
NEXT_PUBLIC_WAFFO_PRODUCT_PRO_MONTHLY。 - 创建年度 Subscription 产品,将产品 ID 保存为
NEXT_PUBLIC_WAFFO_PRODUCT_PRO_YEARLY。 - 创建 One-time 产品,将产品 ID 保存为
NEXT_PUBLIC_WAFFO_PRODUCT_LIFETIME。 新产品默认处于测试模式。接收真实支付前,需要先发布到生产环境;发布是单向 操作。详情请参考 Waffo 订阅指南 和发布产品。
添加环境变量
在 .env 中设置支付提供商和 Waffo 凭据:
NEXT_PUBLIC_PAYMENT_PROVIDER=waffo
# 仅服务端使用的凭据
WAFFO_MERCHANT_ID=MER_...
WAFFO_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----"
# 可选:在生产构建中接受沙盒 Webhook,仅用于冒烟测试
# WAFFO_DEBUG=true
# 产品 ID
NEXT_PUBLIC_WAFFO_PRODUCT_PRO_MONTHLY=PROD_...
NEXT_PUBLIC_WAFFO_PRODUCT_PRO_YEARLY=PROD_...
NEXT_PUBLIC_WAFFO_PRODUCT_LIFETIME=PROD_...配置价格
保留 src/config/website.tsx 中现有的 price.plans 配置,但在 Waffo
启用时,让其中的 priceId 使用 Waffo 产品 ID。金额、货币、周期和计划元数据
必须与 Waffo 中创建的产品保持一致。
price: {
plans: {
pro: {
id: 'pro',
prices: [
{
type: PaymentTypes.SUBSCRIPTION,
priceId: process.env.NEXT_PUBLIC_WAFFO_PRODUCT_PRO_MONTHLY!,
amount: 990,
currency: 'USD',
interval: PlanIntervals.MONTH,
},
{
type: PaymentTypes.SUBSCRIPTION,
priceId: process.env.NEXT_PUBLIC_WAFFO_PRODUCT_PRO_YEARLY!,
amount: 9900,
currency: 'USD',
interval: PlanIntervals.YEAR,
},
],
isFree: false,
isLifetime: false,
},
},
},核心功能
- 月度和年度周期订阅
- 终身一次性支付
- 托管结账,无需在服务端处理卡信息
- 处理支付、订阅和退款事件的 Webhook
- Waffo 负责税务、合规和结算
- 支持 Magic Link 登录的消费者门户
- 与支付提供商无关的价格、结账和账单组件
开发环境
本地开发时,Waffo 必须能够访问您的 Webhook 端点。请通过 HTTPS 隧道暴露本地 服务器:
ngrok http 3000
# 或
cloudflared tunnel --url http://localhost:3000将隧道 URL 作为测试环境 Webhook,例如
https://xxxx.ngrok-free.app/api/webhooks/waffo。不要把临时隧道 URL 配置为生产
Webhook。不要使用 localtunnel,因为它可能会剥离 X-Waffo-Signature 请求头。
模板提供 Waffo 沙盒 E2E 测试套件:
pnpm e2e:waffo沙盒结账不会产生真实扣款。在托管结账页选择 Credit/Debit Card,使用下方的测试卡直接填写沙箱卡片字段。自动化测试避免使用 Quick Fill,因为它可能导致订阅确认一直等待。运行本地 E2E helper 时,可以通过
设置 WAFFO_ENV_FILE 复用被忽略的 MkSaaS Template 环境变量文件。
支付完成后,在 Waffo 托管确认页点击 Done。浏览器会返回
/payment?checkout_id=...,等待对应的 Webhook 支付记录,然后跳转到 Billing。
自动化沙箱配置、服务器端口、环境文件优先级和数据库准备步骤见自动化测试及模板中的 tests/e2e/waffo/README.md。
生产环境
- 完成 Waffo 商店审核 / KYB,启用生产支付。
- 将订阅和一次性产品从测试环境发布到生产环境。
- 创建生产环境 API 凭据,并将
WAFFO_MERCHANT_ID和WAFFO_PRIVATE_KEY替换为生产凭据。 - 将
https://YOUR-DOMAIN.com/api/webhooks/waffo注册为生产 Webhook。 - 不要设置
WAFFO_DEBUG,或将其设置为false。生产构建不能让沙盒事件授予真实 权限。
消费者门户
Waffo 提供共享的消费者门户:
https://pancake.waffo.ai/consumer/portal/login。客户使用购买时填写的邮箱和
一次性 Magic Link 登录,可以查看订阅和发票、取消或重新激活订阅、更新账单信息并
申请退款。
支付确认
Waffo 不会在 Webhook 载荷中返回 CHK_* Checkout Session ID。创建结账前,
MkSaaS 会生成唯一支付引用并作为 orderMerchantExternalId 发送。Waffo 将该引用
带入 Webhook,MkSaaS 再把它保存到 payment.sessionId。
支付结果页只轮询当前登录用户与该引用对应的记录,因此旧的已支付订单不会误完成 新的结账流程。
Webhook 事件
为 /api/webhooks/waffo 配置以下事件:
| 事件 | 描述 |
|---|---|
order.completed | 一次性订单支付成功 |
subscription.activated | 订阅首次支付成功 |
subscription.payment_succeeded | 仅表示支付收据,不更新订阅生命周期状态 |
subscription.renewed | 续费完成,推进当前周期并保持权益有效 |
subscription.recovered | 先前失败的订阅已恢复并重新激活 |
subscription.plan_changed | 订阅产品变更;新产品元数据优先于原订单元数据 |
subscription.canceling | 已申请取消,当前周期结束前仍有效 |
subscription.uncanceled | 撤回取消申请 |
subscription.canceled | 订阅终止 |
subscription.past_due | 续费失败,Waffo 正在重试 |
refund.succeeded | 退款完成并撤销访问权限 |
refund.failed | 退款失败 |
Waffo 会将测试和生产事件发送到同一个端点,每个负载都包含 mode 字段。MkSaaS
会拒绝与当前运行环境不匹配的事件。
Webhook 响应与重试
| 结果 | HTTP 状态码 |
|---|---|
| Waffo 不是当前配置的提供商 | 404 |
缺少载荷或 X-Waffo-Signature 请求头 | 400 |
| 签名无效、请求头格式错误或时间戳过期 | 400 |
| 处理成功或安全去重 | 200 |
| 提供商或数据库临时失败 | 500 |
临时失败会返回 500,让 Waffo 自动重试。处理器保持幂等,重复成功投递不会创建
新的支付记录。
任意成功的 refund.succeeded 事件都会撤销权益。Waffo 没有提供可靠的部分退款
标记,因此部分退款也会撤销权益;如果这不符合您的策略,需要手动处理。
测试卡
在 Waffo 测试模式中使用任意未来有效期和任意三位 CVC:
| 卡号 | 类型 | 结果 |
|---|---|---|
4576 7500 0000 0110 | Visa Credit | 成功 |
2226 9000 0000 0110 | Mastercard Credit | 成功 |
4576 7500 0000 0220 | Visa Credit | 拒绝 |
最佳实践
- 只在服务端保存
WAFFO_PRIVATE_KEY和WAFFO_MERCHANT_ID。 - 使用 Raw Webhook 格式,确保
X-Waffo-Signature请求头不丢失。 - 使用与运行环境匹配的测试或生产凭据。
- 上线前在沙盒中完成一次完整的结账和 Webhook 往返流程。
MkSaaS文档