Documentation Index
Fetch the complete documentation index at: https://docs.waffo.ai/llms.txt
Use this file to discover all available pages before exploring further.
概述
Webhook 在事件发生时向您的服务器发送实时通知 — 订单、支付、订阅和退款。| 渠道 | 投递目标 | 载荷格式 |
|---|---|---|
http | 您的 HTTPS 端点 | RSA-SHA256 签名的 JSON 信封 |
feishu | 飞书 / Lark 机器人入站 webhook | 飞书交互卡片 |
discord | Discord 频道 webhook | Embed 消息(Discord 格式) |
telegram | Telegram 机器人 sendMessage URL | HTML 格式文本 |
slack | Slack 入站 webhook | 含 mrkdwn 字段的 Attachment |
http 渠道使用本页所述的 JSON 信封和签名验证 — 本指南大部分内容覆盖该渠道。对于 IM 类渠道,载荷使用各平台原生格式,认证由 URL 中的 token 完成;无需验证签名。
配置步骤
选择渠道并准备 URL
- HTTP — 构建一个接受 POST 请求并返回
200的服务端端点。 - 飞书 / Discord / Telegram / Slack — 在目标平台创建机器人或入站 webhook,复制其 URL。Telegram 还需记录接收消息的 chat ID。
(仅 HTTP)获取验签公钥
前往 控制台 → 设置 → Webhooks,复制目标环境(Test / Production)的 Webhook Public Key。
注册 Webhook
在 控制台 → 设置 → Webhooks 中添加 webhook,或调用
POST /v1/actions/store/add-webhook。每条 webhook 记录指定一个渠道、一个 URL、订阅的事件以及目标环境(Test 设 testMode: true,Production 设 false)。每个门店可注册多条 webhook。环境隔离
每条 webhook 通过testMode 标志注册到单一环境。Test 和 Production 完全独立:
| 项目 | Test | Production |
|---|---|---|
| Webhook 记录 | testMode: true | testMode: false |
| 签名密钥(仅 HTTP) | Test 密钥对 | Production 密钥对 |
| 验签公钥(仅 HTTP) | Dashboard Test 公钥 | Dashboard Production 公钥 |
| 投递时的选择器 | 请求头 X-Environment: test | 请求头 X-Environment: prod(或省略) |
mode 字段表示来源环境:"test" 或 "prod"。
载荷格式
请求头
| 请求头 | 说明 |
|---|---|
Content-Type | application/json |
X-Waffo-Signature | 签名字符串:t=<timestamp>,v1=<signature> |
X-Waffo-Event | 事件类型(例如 order.completed) |
请求体
顶层字段
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 事件实体 ID — 对大多数事件与 eventId 相同 |
timestamp | string | 事件时间(ISO 8601 UTC) |
eventType | string | 事件类型(参见 事件类型) |
eventId | string | 业务事件标识符 — 按事件类型映射到不同实体(参见 eventId 映射) |
storeId | string | Store ID |
storeName | string | 商店名称 |
mode | string | "test" 或 "prod" |
data 字段
data 对象包含交易详情。部分字段始终存在,其他字段仅在特定事件类型或数据可用时出现。
始终存在:
| 字段 | 类型 | 说明 |
|---|---|---|
orderId | string | 订单 ID |
orderStatus | string | 订单状态(例如 "completed"、"active"、"canceling") |
buyerEmail | string | 买家邮箱地址 |
currency | string | ISO 4217 货币代码(例如 "USD"、"JPY") |
amount | string | 交易总额含税(显示格式,例如 "29.00") |
taxAmount | string | 税额(显示格式,例如 "2.90") |
productName | string | 商品名称 |
orderMetadata | object | 结账会话中的订单级元数据(商户定义的键值对) |
productMetadata | object | 创建/更新商品时设置的商品级元数据 |
| 字段 | 类型 | 出现条件 | 说明 |
|---|---|---|---|
merchantProvidedBuyerIdentity | string | 在结账时设置 | 商户自定义的买家标识 |
billingDetail | object | 在结账时设置 | 账单地址(country、isBusiness 等) |
taxRate | number | 应用了税率 | 税率小数(例如 0.1 表示 10%) |
taxName | string | 应用了税率 | 税种名称(例如 "Consumption Tax") |
subtotal | string | 可用时 | 税前小计(显示格式) |
total | string | 可用时 | 税后合计(显示格式) |
productDescription | string | 商品已设置 | 商品描述 |
order.completed、subscription.payment_succeeded):
| 字段 | 类型 | 说明 |
|---|---|---|
paymentId | string | 支付 ID |
paymentStatus | string | 支付状态("succeeded"、"failed") |
paymentMethod | string | 支付方式(例如 "card") |
paymentLast4 | string | 支付工具末 4 位 |
paymentDate | string | 支付日期(ISO 8601 日期,例如 "2026-03-10") |
paymentFailureReason | string | 失败原因(支付失败时) |
subscription.*):
| 字段 | 类型 | 说明 |
|---|---|---|
billingPeriod | string | "weekly"、"monthly"、"quarterly"、"yearly" |
currentPeriodStart | string | 当前计费周期开始日期(ISO 8601 日期) |
currentPeriodEnd | string | 当前计费周期结束日期(ISO 8601 日期) |
canceledAt | string | 取消时间戳(ISO 8601,取消中/已取消时存在) |
refund.succeeded、refund.failed):
| 字段 | 类型 | 说明 |
|---|---|---|
refundStatus | string | "succeeded" 或 "failed" |
refundReason | string | 退款原因 |
refundCreatedAt | string | 退款创建时间戳(ISO 8601) |
paymentId | string | 原始支付 ID |
paymentStatus | string | 原始支付状态 |
paymentMethod | string | 原始支付方式 |
paymentLast4 | string | 原始支付末 4 位 |
paymentDate | string | 原始支付日期 |
金额为显示格式字符串,已从最小单位转换。例如,USD
"29.00" = 2900 美分;JPY "4500" = 4500 日元。有值时可使用 subtotal 和 total 进行明细展示。eventId 映射
eventId 标识触发事件的业务实体:
| 事件类型 | eventId 映射到 | 示例 |
|---|---|---|
order.completed | Payment ID | PAY_6eYCunG3IMmIgcQOnaXdoA |
subscription.activated | Order ID | ORD_5dXBtmF2HLlHfbPNm0Wcnz |
subscription.payment_succeeded | Payment ID | PAY_6eYCunG3IMmIgcQOnaXdoA |
subscription.canceling | Order ID | ORD_5dXBtmF2HLlHfbPNm0Wcnz |
subscription.uncanceled | Order ID | ORD_5dXBtmF2HLlHfbPNm0Wcnz |
subscription.updated | Order ID | ORD_5dXBtmF2HLlHfbPNm0Wcnz |
subscription.canceled | Order ID | ORD_5dXBtmF2HLlHfbPNm0Wcnz |
subscription.past_due | Order ID + 月份 | ORD_5dXBtmF2HLlHfbPNm0Wcnz-2026-04 |
refund.succeeded | Refund ID | REF_4cWAtlE1GKkGebONl9Xbnx |
refund.failed | Refund ID | REF_4cWAtlE1GKkGebONl9Xbnx |
subscription.past_due 会在 eventId 后追加 -YYYY-MM。同一订阅每个自然月最多触发一次 past_due 事件。若下月仍逾期,将触发新事件。事件类型
概览
| 事件 | 触发条件 | eventId |
|---|---|---|
order.completed | 一次性订单支付成功 | Payment ID |
subscription.activated | 订阅首次支付成功 | Order ID |
subscription.payment_succeeded | 续期支付成功(非首次) | Payment ID |
subscription.canceling | 请求取消 — 当前付费期结束前仍有效 | Order ID |
subscription.uncanceled | 撤回取消 | Order ID |
subscription.updated | 产品变更(升级/降级) | Order ID |
subscription.canceled | 订阅终止(付费期结束) | Order ID |
subscription.past_due | 续期支付失败 | Order ID + 月份 |
refund.succeeded | 退款完成 | Refund ID |
refund.failed | 退款失败 | Refund ID |
subscription.uncanceled 和 subscription.updated 事件模板已就绪,将在相应功能上线后激活。事件详情
order.completed
order.completed
触发条件:一次性订单首次支付成功。载荷:
data.amount— 支付金额(含税)data.orderId— 一次性订单 ID
- 交付数字商品(许可证密钥、下载链接、激活码)
- 更新您的订单管理系统
- 向买家发送确认(如果未使用 Waffo 内置邮件)
order.completed。退款通过 refund.succeeded / refund.failed 通知。subscription.activated
subscription.activated
触发条件:新订阅的首次支付成功(
pending → active)。载荷:data.amount— 首次支付金额(含税)data.productName— 订阅产品名称
- 为订阅者开通账户和授予访问权限
- 记录订阅开始日期
pending 转为 active 时触发。后续续期使用 subscription.payment_succeeded。subscription.payment_succeeded
subscription.payment_succeeded
触发条件:周期性续期支付成功(非首次支付)。载荷:
data.amount— 本期续费金额(含税)data.orderId— 订阅订单 ID
- 延长服务期限
- 为本计费周期生成发票
- 如果订阅之前处于
past_due状态,恢复完整访问权限
subscription.canceling
subscription.canceling
触发条件:买家或商户请求取消。订阅在当前付费期结束前仍保持有效。建议操作:
- 显示”订阅将于 [日期] 到期”提示
- 提供挽留流程(例如折扣续费)
- 不要撤回访问权限 — 买家已为当前周期付费
subscription.uncanceled)。subscription.uncanceled
subscription.uncanceled
触发条件:在当前周期结束前撤回取消。建议操作:
- 移除”即将到期”提示
- 恢复自动续费状态
subscription.updated
subscription.updated
触发条件:订阅产品变更(升级或降级)。载荷:
data.productName— 变更后的新产品名称data.amount— 新金额
- 更新买家的访问级别(添加/移除功能)
- 更新计费记录
subscription.canceled
subscription.canceled
触发条件:订阅已终止 — 付费期结束,不会再有续费。建议操作:
- 撤回访问权限(或降级到免费版)
- 保留数据一段宽限期(以防买家重新订阅)
- 发送”订阅已结束”确认
subscription.past_due
subscription.past_due
触发条件:续期支付失败,订阅进入逾期状态。载荷:
data.amount— 本期应付金额eventId— 格式:{orderId}-YYYY-MM(按月去重)
- 通知买家更新支付方式
- 可选择降级服务(限制功能而非完全撤回)
- 不要立即撤回访问权限 — PSP 可能会自动重试扣款
past_due 事件。若下月仍逾期,将触发新事件。refund.succeeded
refund.succeeded
触发条件:退款已完成,资金已退回。载荷:
data.amount— 退款金额(含税)data.orderId— 原始订单 ID
- 撤销已交付的数字商品(吊销许可证、禁用下载)
- 将订单状态更新为”已退款”
refund.failed
refund.failed
触发条件:退款处理失败。建议操作:
- 记录失败信息以供人工审核
- 不要撤销商品(退款未完成)
订阅生命周期
| 终态 | 含义 | 触发 Webhook? |
|---|---|---|
canceled | 订阅终止(买家/商户取消或逾期) | subscription.canceled |
closed | 从未激活 — 支付超时 | 否 |
expired | 固定期限订阅自然到期 | 否 |
签名验证
在生产环境中务必验证签名。 不验证签名的话,任何人都可以向您的端点发送伪造请求。算法
使用 SDK(推荐)
SDK 内置公钥,自动检测环境,并处理格式标准化:手动验证
如果您不使用 TypeScript SDK,请手动实现签名验证。响应要求
- 返回 2xx 状态码(推荐
200) - 在 10 秒内响应
- 响应体内容无要求
重试策略
投递失败时自动使用指数退避重试:| 项目 | 详情 |
|---|---|
| 重试次数 | 最多 3 次(含首次共 4 次尝试) |
| 策略 | 指数退避 |
| 超时 | 非 2xx 或在超时窗口内无响应 |
| 最终失败 | 所有重试用尽后标记为 failed |
投递状态
| 状态 | 说明 |
|---|---|
pending | 已创建,等待投递或重试中 |
success | 投递成功(您的服务器返回 2xx) |
failed | 所有重试已用尽 |
处理重复事件
网络问题可能导致同一事件被多次投递。确保您的事件处理逻辑是幂等的。 使用eventType + eventId 组合(系统中有唯一约束)进行去重:
同一业务事件(相同的
eventType + eventId)只会创建一条投递记录 — 不会重复创建。但是,由于重试机制,单次投递可能多次到达您的端点。最佳实践
始终验证签名
始终验证签名
始终验证
X-Waffo-Signature。不验证签名的话,任何人都可以向您的端点发送伪造请求。使用 HTTPS
使用 HTTPS
生产环境的 Webhook URL 必须使用 HTTPS 以保护传输中的数据。
快速响应,异步处理
快速响应,异步处理
立即返回
200,在后台处理业务逻辑。响应过慢会导致不必要的重试。使用 eventType + eventId 去重
使用 eventType + eventId 去重
使用
eventType + eventId 组合进行去重。确保同一投递被多次处理不会产生副作用。检查时间戳
检查时间戳
验证
t 时间戳在当前时间的 5 分钟以内,以防止重放攻击。使用正确环境的密钥
使用正确环境的密钥
Test 和 Production 使用不同的密钥对。将公钥与载荷中的
mode 字段匹配。记录接收到的载荷
记录接收到的载荷
存储接收到的载荷用于调试。Dashboard 也提供投递日志查询功能。
处理所有已订阅的事件
处理所有已订阅的事件
为您订阅的每个事件添加处理分支,即使暂时不需要处理。对未处理的事件返回
200 — 返回错误会触发不必要的重试。测试
发送测试事件(推荐)
使用 Dashboard “Send Test Event” 按钮发送测试事件,无需触发真实交易。测试事件使用固定的示例数据(amount 0、taxAmount 0、product “[TEST] Webhook Verification”),始终使用 Test 密钥签名。 支持所有 10 种事件类型 — 逐一测试以验证您的处理程序。使用测试模式
- 在 Dashboard 中配置 Test 环境的 Webhook URL 和事件
- 在 Test 模式下执行真实操作(创建订单、处理支付)
- 事件将发送到您的 Test Webhook URL,使用 Test 签名密钥
本地开发
使用隧道工具暴露您的本地服务器:投递日志
在 Dashboard 中查看 Webhook 投递历史:- 状态:pending / success / failed
- HTTP 状态码:您的服务器的响应码
- 响应体:您的服务器的响应(截断至 1000 字符)
- 时间戳:最后一次投递尝试
常见问题
没有收到 Webhook
- 确认 Dashboard 中已配置 Webhook URL 且可公开访问
- 确认已订阅正确的事件类型
- 确认使用了正确的环境(Test / Production)
- 检查防火墙是否允许来自 Waffo 的请求
- 尝试 Dashboard “Send Test Event” 来定位问题
签名验证失败
- 确认使用了正确环境的公钥(Test 与 Production)
- 确认使用的是原始请求体 — 而非解析后的 JSON 对象
- 检查是否有中间件或代理修改了请求体
- 确认签名输入格式为
${t}.${rawBody}(时间戳 + 点 + 原始请求体) - 如果使用 TypeScript,建议切换到 @waffo/pancake-ts SDK — 它自动处理密钥选择和格式规范化
收到重复事件
这是正常的重试行为。如果您的端点返回非 2xx 或超时,系统会重试。确保您的处理逻辑是幂等的 — 使用eventType + eventId 组合进行去重。
subscription.canceling 和 subscription.canceled 的区别
canceling:已请求取消,但当前付费期尚未结束。订阅仍然有效,买家可以撤回取消(触发uncanceled)。不要撤回访问权限。canceled:订阅已终止。这是不可逆的 — 撤回访问权限或降级权限。
不同事件中 data.amount 的含义
所有事件:data.amount 是该特定事件的交易金额(含税):
order.completed/subscription.activated— 支付金额subscription.payment_succeeded— 本期续费金额subscription.past_due— 本期应付金额refund.succeeded/refund.failed— 退款金额subscription.canceling/subscription.canceled/subscription.uncanceled— 订阅每期金额