外层信封里有什么?
恰好两个键:X-SIGNATURE 请求头中而不是 body 里。所以按 eventType 分发,其他一切从 result 里取。
我应该按 eventType 分发,还是按 result 里的某个字段?
按 eventType 分发。它告诉你拿到的是哪个事件家族,从而决定 result 的形态;result 内的状态字段再告诉你发生了什么。反过来做会在一个事件类型携带多种状态的情况下出问题——例如支付通知在授权阶段和终态时是同一个 eventType。
经验法则:eventType 选择解析器,result 的状态选择业务分支。
哪个字段是我自己的订单号?
每个报文都携带身份的两面——你的和 Waffo 的。用你的作为进入你数据库的关联键,用 Waffo 的与支持沟通。
三个省时间的说明:
paymentRequestId也是你为该请求生成的幂等键,这就是为什么用不同金额或币种复用它会返回A0011。refundRequestId和subscriptionRequest同理。- 拒付把被争议的订单命名为
originalOrderId,而不是其他所有报文使用的acquiringOrderId。它是唯一打破命名模式的报文,在 Portal 的 Webhook 日志中用你自己的订单号搜索拒付找不到它——见 Webhook 事件字典与批量回放。 - 订阅扣款的支付或退款报文还携带嵌套的
subscriptionInfo,包含subscriptionId、period和subscriptionRequest,你借此把支付订单归属到某个账单周期。
卡 BIN 和后四位在哪里?
不在顶层。卡信息在paymentInfo.payMethodResponse 内,重要的结构细节是 payMethodResponse 是 JSON 编码的字符串,不是嵌套对象——你必须先解析才能从中读取任何内容。
解析后,卡相关的键是:
同一字符串中的其他键涵盖汇率、客户支付的金额和币种,以及支付方式参考号——已发布的描述见 API Reference。
**它们何时有值?**仅对卡类支付——卡,以及底层呈现为卡的两种移动钱包产品。对于其他任何支付方式,完全不会出现卡相关的键。所以请把解析器写成把每一个都视为可选;
cardBin 缺失是正常的,不是错误。
firstSix、first6、lastFour、last4、maskedCardNumber 和 cardNo 不是任何 Waffo 报文中的字段名。如果你在找其中之一,你要的是 maskCardData、cardBin 或 cardLast4。
我能从查询或通知中拿到 userFee 吗?
不能。userFee 不是任何面向商户的查询或通知报文中的响应字段。
这个名字确实出现在你绕过托管收银台时使用的 orderAction 数据的描述中——但那是该 action 报文的键,不是订单的字段。订单上确实返回的金额和手续费,见手续费与税费处理。
Payout 有 actFeeAmount——feeAmount 还在吗?我该消费哪个?
消费 actFeeAmount。在面向商户的 Payout 报文上,feeAmount 不存在——actFeeAmount 取代了它,也是已发布契约携带的字段。
请与其两个伴随字段一起读取:
这一关系对你的账务很重要:
feeStatus 为 DEDUCTED 时 actFeeAmount 是已收取的金额,feeStatus 为 RETURNED 或 NONE 时为 0。所以不要在不检查 feeStatus 的情况下把零手续费读成”免费”——退回的手续费和从未收取的手续费都显示为零,含义不同。
一个命名陷阱:feeAmount 确实仍存在于拒付报文上,那里它表示拒付手续费。同一字段名,无关的数量——不要在两者之间共享反序列化器。
为完整起见,Payout 结果字段:status 为 IN_PROGRESS、SUCCESS 或 FAILURE;冲正由 subStatus 为 FULL_REVERSED 或 PARTIAL_REVERSED 表达;失败携带 failureCode 和 failureReason。
客户支付后,我能拿到他们的银行账户以便退回同一账户吗?
不能。没有任何 payin 通知或订单查询返回客户的银行账号或账户持有人姓名。 三个字段看起来像是,但都不是:
答案中令人安心的部分:对于退回原工具重要的支付方式,退款沿原支付路径执行,而不是退到你指定的账户——所以你不需要账号就能得到这一行为。当退款确实需要银行信息时,API 会向你索要。见退款资金来源和支付方式退款限制。
未支付、已取消或超时的订单在报文里长什么样?
三者都以PAYMENT_NOTIFICATION 到达,都落在同一个终态——ORDER_CLOSE。没有单独的已取消、已过期或失败状态值。
你可能收到的订单状态完整集合:
要区分取消、超时和拒绝,读取
orderFailedReason——也是一个 JSON 编码的字符串,形态为 {"orderFailedCode":"...","orderFailedDescription":"..."}。它仅在状态为 ORDER_CLOSE 时存在;成功或进行中的订单完全省略它,所以你的解析器必须容忍其缺失而不是给默认值。
退款报文的等价物是 refundFailedReason,订阅的是 failedReason,都是同样的 JSON 字符串形态。failReason、errorCode、errorMsg、channelErrorCode 和 merchantErrorCode 这些名字在这些报文中不存在。
订阅上的 paymentDetails 是完整历史吗?会一直增长吗?
两个都是,而且值得在你有长期订阅者之前就为此设计。paymentDetails 是截至当前周期的每一期的数组,按周期升序,没有上限、没有分页大小、没有游标。运行多年的月度订阅每期给数组增加一个元素,报文随之增长。
每个元素携带:
两个实际后果:
- **流式处理或建索引,不要整体持有。**读取你关心的周期——通常是最大的
period,或你尚未记录的那些——而不是在每次通知时对整个数组做 diff。 - **
paymentDetails[].orderStatus是该期订单自身的状态,不是商户展示的订单状态。**除PAY_SUCCESS和ORDER_CLOSE外,在扣款仍待处理或重试时它可以是INIT、TO_PAY或NEED_RETRY。只把前两个当作终态。
paymentDetails 是一个标志(0 或 1,默认 0),请求返回该数组;在响应上它就是数组。
我的订阅仍是 ACTIVE 但某一期扣款失败了,我会收到什么?
你会收到SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION,且只在该期重试耗尽之后。中间的失败尝试不触发任何通知——该期订单进入 NEED_RETRY 并等待。
报文告诉你的:
对原因要设定预期,因为这是一个真实的限制:顶层
failedReason 是订阅的失败详情,不是失败周期的,所以它不能可靠地描述那一期为何被拒,周期元素也不携带自己的失败码。要获得按次尝试的失败原因,请同时订阅 PAYMENT_NOTIFICATION——它对每次支付尝试触发并携带该次尝试的 orderFailedReason。这正是订阅通知选择指南在”最小”和”完整”组合之间所做的区分。
续订重试用尽后,订阅会被取消吗?
取决于哪一期失败,两种情况行为不同:
所以不要把耗尽的续订当作取消,也不要仅凭周期变更通知撤销权益。如果你的产品应在某期失败后暂停访问,那是你要在周期事件上实现的策略——Waffo 不会替你关闭订阅。
**重试多少次?**没有单一数字:重试次数和间隔来自你商户协议的续订重试策略,按周期类型解析——每日、每周、每月、每季度、每年,或默认。请向集成支持确认你账户配置的数字而不是假设,并见订阅重试与支付方式。
客户在钱包应用内取消了,我拿到哪个状态?
subscriptionStatus 为 USER_CANCELLED 的 SUBSCRIPTION_STATUS_NOTIFICATION。
完整的状态集合,方便你对全部分支处理:
注意
PLATFORM_CANCELLED 虽然可能被投递,却不在订阅状态字段已发布的枚举列表中,所以请把它作为取消家族的一部分处理——MERCHANT_CANCELLED、USER_CANCELLED、CHANNEL_CANCELLED 和 PLATFORM_CANCELLED 都意味着”不再计费”。
通知 body 真的与查询响应完全相同吗?
大多数情况是,但并非总是——而例外正是会让你意外的那个。
两个含义:
- **不要通过与查询响应做 diff 来构建你的订阅处理程序。**多出来的数组是预期的,不是 bug,也是订阅 Webhook 可能比你对比的查询大得多的原因。
- **不要假设键的顺序稳定。**通知 body 中的字段顺序不保证与任何东西一致,这只在你打算自己对 body 做哈希或规范化时才有影响。你不需要这样做:对收到的原始字节验证
X-SIGNATURE。见 Webhook 签名验证。
能给我每种事件类型一份完整的真实示例 body 吗?
按事件的示例维护在 API Reference 而不是这里,因为每个result 就是对应的查询响应,而 API Reference 由实时契约生成——这使它成为唯一不会漂移的副本。
把它当作 fixture 来源时的两个提醒:
- 已发布的参考会周期性重新生成,所以报文中存在的字段可能一段时间内在参考中缺失。
payMethodResponse内的卡键和订阅加购详情就是当前的例子:它们会被投递,却只被部分描述。把报文当作超集,让你的解析器容忍未知字段。 - **能捕获真实报文时不要从文档构建 fixture。**在沙箱中跑通流程并记录到达你端点的内容——那是你商户配置下、带你的支付方式的报文,也是你的处理程序真正要应对的。见 Sandbox Simulation Limits FAQ。