> ## Documentation Index
> Fetch the complete documentation index at: https://waffo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook 报文字段 FAQ

> Webhook body 中哪个字段是你的订单号、卡 BIN 和后四位实际在哪里、该消费哪些 Payout 手续费字段、失败的订阅周期如何表达，以及通知 body 与对应查询响应的差异之处。

事件列表和"这对应哪个查询"的映射在 [Webhook 事件类型列表](/docs/zh/developer-docs/webhook/event-types)；完整字段 schema 在 [API Reference](/docs/api-reference/introduction)。投递规则——超时、接受的响应、重试、URL 解析——在 [Webhook 投递契约 FAQ](/docs/zh/support/webhook-delivery-contract-faq)。

**本页回答字段级的问题**——那些你在写处理程序时找不到预期值会遇到的问题。

## 外层信封里有什么？

恰好两个键：

```json theme={null}
{
  "eventType": "PAYMENT_NOTIFICATION",
  "result": { }
}
```

顶层**没有**时间戳、没有通知 id、没有版本、没有商户 id，签名在 `X-SIGNATURE` 请求头中而不是 body 里。所以按 `eventType` 分发，其他一切从 `result` 里取。

## 我应该按 `eventType` 分发，还是按 `result` 里的某个字段？

按 `eventType` 分发。它告诉你拿到的是哪个事件家族，从而决定 `result` 的形态；`result` 内的状态字段再告诉你*发生了什么*。反过来做会在一个事件类型携带多种状态的情况下出问题——例如支付通知在授权阶段和终态时是同一个 `eventType`。

经验法则：`eventType` 选择解析器，`result` 的状态选择业务分支。

## 哪个字段是我自己的订单号？

每个报文都携带身份的两面——你的和 Waffo 的。用你的作为进入你数据库的关联键，用 Waffo 的与支持沟通。

| 报文 | 你的标识 | Waffo 的标识 |
| - | - | - |
| 支付 | `merchantOrderId`、`paymentRequestId` | `acquiringOrderId` |
| 退款 | `merchantRefundOrderId`、`refundRequestId`、`origPaymentRequestId` | `acquiringRefundOrderId`、`acquiringOrderId` |
| 订阅 | `subscriptionRequest`、`merchantSubscriptionId` | `subscriptionId` |
| Payout | `payoutRequestId` | `payoutOrderId` |
| 拒付 | `merchantOrderId`、`originalPaymentRequestId` | `chargebackId`、`originalOrderId` |

三个省时间的说明：

* **`paymentRequestId` 也是你为该请求生成的幂等键**，这就是为什么用不同金额或币种复用它会返回 `A0011`。`refundRequestId` 和 `subscriptionRequest` 同理。
* **拒付把被争议的订单命名为 `originalOrderId`**，而不是其他所有报文使用的 `acquiringOrderId`。它是唯一打破命名模式的报文，在 Portal 的 Webhook 日志中用你自己的订单号搜索拒付找不到它——见 [Webhook 事件字典与批量回放](/docs/zh/support/webhook-events-and-resend-faq)。
* 订阅扣款的支付或退款报文还携带嵌套的 `subscriptionInfo`，包含 `subscriptionId`、`period` 和 `subscriptionRequest`，你借此把支付订单归属到某个账单周期。

## 卡 BIN 和后四位在哪里？

不在顶层。卡信息在 `paymentInfo.payMethodResponse` 内，重要的结构细节是 **`payMethodResponse` 是 JSON 编码的*字符串*，不是嵌套对象**——你必须先解析才能从中读取任何内容。

解析后，卡相关的键是：

| 键 | 内容 |
| - | - |
| `maskCardData` | 前六位和后四位合成一个掩码字符串，例如 `402856******9917` |
| `cardBin` | BIN，6–8 位 |
| `cardLast4` | 后四位 |
| `cardExpiry` | 卡有效期 |
| `cardScheme` | 卡组织 |
| `cardType`、`subCardType` | 卡类别 |
| `cardIssueCountryCode` | 发卡国家 |
| `cardIssuerName` | 发卡行名称 |
| `schemeTransactionId` | 卡组织级交易参考号 |
| `tokenId` | 支付使用了 token 时的 token |

同一字符串中的其他键涵盖汇率、客户支付的金额和币种，以及支付方式参考号——已发布的描述见 [API Reference](/docs/api-reference/introduction)。

\*\*它们何时有值？\*\*仅对卡类支付——卡，以及底层呈现为卡的两种移动钱包产品。对于其他任何支付方式，完全不会出现卡相关的键。所以请把解析器写成把每一个都视为可选；`cardBin` 缺失是正常的，不是错误。

`firstSix`、`first6`、`lastFour`、`last4`、`maskedCardNumber` 和 `cardNo` **不是**任何 Waffo 报文中的字段名。如果你在找其中之一，你要的是 `maskCardData`、`cardBin` 或 `cardLast4`。

## 我能从查询或通知中拿到 `userFee` 吗？

不能。`userFee` 不是任何面向商户的查询或通知报文中的响应字段。

这个名字确实出现在你绕过托管收银台时使用的 `orderAction` 数据的描述中——但那是该 action 报文的键，不是订单的字段。订单上*确实*返回的金额和手续费，见[手续费与税费处理](/docs/zh/support/fee-and-tax-treatment-faq)。

## Payout 有 `actFeeAmount`——`feeAmount` 还在吗？我该消费哪个？

消费 **`actFeeAmount`**。在面向商户的 Payout 报文上，`feeAmount` 不存在——`actFeeAmount` 取代了它，也是已发布契约携带的字段。

请与其两个伴随字段一起读取：

| 字段 | 含义 |
| - | - |
| `actFeeAmount` | 交易完成时实际收取的手续费 |
| `feeCurrency` | `actFeeAmount` 的计价币种 |
| `feeStatus` | `DEDUCTED`、`RETURNED` 或 `NONE` |

这一关系对你的账务很重要：`feeStatus` 为 `DEDUCTED` 时 `actFeeAmount` 是已收取的金额，**`feeStatus` 为 `RETURNED` 或 `NONE` 时为 `0`**。所以不要在不检查 `feeStatus` 的情况下把零手续费读成"免费"——退回的手续费和从未收取的手续费都显示为零，含义不同。

一个命名陷阱：`feeAmount` *确实*仍存在于**拒付**报文上，那里它表示拒付手续费。同一字段名，无关的数量——不要在两者之间共享反序列化器。

为完整起见，Payout 结果字段：`status` 为 `IN_PROGRESS`、`SUCCESS` 或 `FAILURE`；冲正由 `subStatus` 为 `FULL_REVERSED` 或 `PARTIAL_REVERSED` 表达；失败携带 `failureCode` 和 `failureReason`。

## 客户支付后，我能拿到他们的银行账户以便退回同一账户吗？

不能。没有任何 payin 通知或订单查询返回客户的银行账号或账户持有人姓名。

三个字段看起来像是，但都不是：

| 字段 | 实际是什么 |
| - | - |
| `paymentInfo.payMethodUserAccountNo`（配合 `payMethodUserAccountType` 为 `EMAIL`、`PHONE_NO` 或 `ACCOUNT_ID`） | **你在创建订单时提供**的账户标识，原样回传 |
| `orderAction` 内的银行信息 | 客户**付入**的 Waffo 侧账户——方向相反 |
| **退款**报文上的 `userInfo.userBankInfo` | **你在退款请求上提供**的银行信息，某些支付方式类型执行付款环节时要求 |

答案中令人安心的部分：对于退回原工具重要的支付方式，退款沿原支付路径执行，而不是退到你指定的账户——所以你不需要账号就能得到这一行为。当退款确实需要银行信息时，API 会向你索要。见[退款资金来源](/docs/zh/support/refund-funding-source-faq)和[支付方式退款限制](/docs/zh/support/payment-method-refund-limits-faq)。

## 未支付、已取消或超时的订单在报文里长什么样？

三者都以 `PAYMENT_NOTIFICATION` 到达，都落在同一个终态——**`ORDER_CLOSE`**。没有单独的已取消、已过期或失败状态值。

你可能收到的订单状态完整集合：

| `orderStatus` | 是否终态 | 含义 |
| - | - | - |
| `AUTHORIZATION_REQUIRED` | 否 | 等待客户授权 |
| `PAY_IN_PROGRESS` | 否 | 支付进行中 |
| `AUTHED_WAITING_CAPTURE` | 否 | 已授权；等待你请款 |
| `CAPTURE_IN_PROGRESS` | 否 | 请款进行中 |
| `PAY_SUCCESS` | 是 | 已支付 |
| `ORDER_CLOSE` | 是 | 未支付 |

要区分取消、超时和拒绝，读取 **`orderFailedReason`**——也是一个 **JSON 编码的字符串**，形态为 `{"orderFailedCode":"...","orderFailedDescription":"..."}`。它**仅在**状态为 `ORDER_CLOSE` 时存在；成功或进行中的订单完全省略它，所以你的解析器必须容忍其缺失而不是给默认值。

退款报文的等价物是 `refundFailedReason`，订阅的是 `failedReason`，都是同样的 JSON 字符串形态。`failReason`、`errorCode`、`errorMsg`、`channelErrorCode` 和 `merchantErrorCode` 这些名字在这些报文中**不**存在。

## 订阅上的 `paymentDetails` 是完整历史吗？会一直增长吗？

两个都是，而且值得在你有长期订阅者之前就为此设计。`paymentDetails` 是**截至当前周期的每一期**的数组，按周期升序，**没有上限、没有分页大小、没有游标**。运行多年的月度订阅每期给数组增加一个元素，报文随之增长。

每个元素携带：

| 字段 | 内容 |
| - | - |
| `acquiringOrderId` | 该期的支付订单 |
| `period` | 周期号 |
| `orderCurrency`、`orderAmount` | 该期的扣款 |
| `orderStatus` | 该期订单的状态 |
| `orderUpdatedAt` | 上次变化时间 |
| `topupDetails` | 该期的加购订单（如有） |

两个实际后果：

* \*\*流式处理或建索引，不要整体持有。\*\*读取你关心的周期——通常是最大的 `period`，或你尚未记录的那些——而不是在每次通知时对整个数组做 diff。
* \*\*`paymentDetails[].orderStatus` 是该期订单自身的状态，不是商户展示的订单状态。\*\*除 `PAY_SUCCESS` 和 `ORDER_CLOSE` 外，在扣款仍待处理或重试时它可以是 `INIT`、`TO_PAY` 或 `NEED_RETRY`。只把前两个当作终态。

一个相关的不对称：同一个名字在订阅查询契约中表示两种不同的东西。在**请求**上 `paymentDetails` 是一个标志（`0` 或 `1`，默认 `0`），请求返回该数组；在**响应**上它就是数组。

## 我的订阅仍是 ACTIVE 但某一期扣款失败了，我会收到什么？

你会收到 **`SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION`**，且只在**该期重试耗尽之后**。中间的失败尝试不触发任何通知——该期订单进入 `NEED_RETRY` 并等待。

报文告诉你的：

| 位置 | 内容 |
| - | - |
| `subscriptionStatus` | 仍为 `ACTIVE`——续订周期失败不会关闭订阅 |
| `paymentDetails[].period` | 哪一期失败了 |
| `paymentDetails[].orderStatus` | 该期为 `ORDER_CLOSE` |
| `productInfo.currentPeriod` | 订阅当前所在的周期 |

对原因要设定预期，因为这是一个真实的限制：顶层 `failedReason` 是**订阅**的失败详情，不是失败周期的，所以它不能可靠地描述那一期为何被拒，周期元素也不携带自己的失败码。要获得按次尝试的失败原因，请同时订阅 `PAYMENT_NOTIFICATION`——它对每次支付尝试触发并携带该次尝试的 `orderFailedReason`。这正是[订阅通知选择指南](/docs/zh/developer-docs/webhook/event-types)在"最小"和"完整"组合之间所做的区分。

## 续订重试用尽后，订阅会被取消吗？

取决于哪一期失败，两种情况行为不同：

| 哪一期耗尽重试 | 订阅结果 | 事件 |
| - | - | - |
| 首次扣款（包括延期开始的激活扣款） | 订阅变为 `CLOSE` | `subscriptionStatus` 为 `CLOSE` 的 `SUBSCRIPTION_STATUS_NOTIFICATION` |
| 续订周期 | 订阅**保持 `ACTIVE`**，下一期仍会尝试 | 仅 `SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION`——没有状态通知 |

所以不要把耗尽的续订当作取消，也不要仅凭周期变更通知撤销权益。如果你的产品应在某期失败后暂停访问，那是你要在周期事件上实现的策略——Waffo 不会替你关闭订阅。

\*\*重试多少次？\*\*没有单一数字：重试次数和间隔来自你商户协议的续订重试策略，按周期类型解析——每日、每周、每月、每季度、每年，或默认。请向集成支持确认你账户配置的数字而不是假设，并见[订阅重试与支付方式](/docs/zh/support/subscription-retry-and-payment-methods-faq)。

## 客户在钱包应用内取消了，我拿到哪个状态？

**`subscriptionStatus` 为 `USER_CANCELLED` 的 `SUBSCRIPTION_STATUS_NOTIFICATION`。**

完整的状态集合，方便你对全部分支处理：

| `subscriptionStatus` | 由谁设置 |
| - | - |
| `IN_PROGRESS` | 订阅已创建，首次扣款进行中 |
| `AUTHORIZATION_REQUIRED` | 等待客户授权 |
| `ACTIVE` | 生效中 |
| `CLOSE` | 首次扣款最终失败后关闭 |
| `MERCHANT_CANCELLED` | 你的取消 API 调用，或从 Portal 取消 |
| `USER_CANCELLED` | 客户在钱包或订阅管理页面取消 |
| `CHANNEL_CANCELLED` | 底层支付工具在支付方式侧被解绑 |
| `PLATFORM_CANCELLED` | 由 Waffo 取消——唯一会填充 `cancelReason` 的状态 |
| `EXPIRED` | 订阅的结束时间已过 |

注意 `PLATFORM_CANCELLED` 虽然可能被投递，却不在订阅状态字段已发布的枚举列表中，所以请把它作为取消家族的一部分处理——`MERCHANT_CANCELLED`、`USER_CANCELLED`、`CHANNEL_CANCELLED` 和 `PLATFORM_CANCELLED` 都意味着"不再计费"。

## 通知 body 真的与查询响应完全相同吗？

大多数情况是，但并非总是——而例外正是会让你意外的那个。

| 报文 | 与查询 `data` 相同？ |
| - | - |
| 支付、退款、拒付、订阅变更 | 是——由与对应查询相同的代码构建 |
| 订阅状态 / 周期变更 | **否。**通知**始终**包含完整的 `paymentDetails` 数组；订阅查询除非你要求否则省略它 |
| Payout | 与 Payout 查询类型相同，另加你传入的 `notifyUrl` 在 body 中回传 |

两个含义：

* \*\*不要通过与查询响应做 diff 来构建你的订阅处理程序。\*\*多出来的数组是预期的，不是 bug，也是订阅 Webhook 可能比你对比的查询大得多的原因。
* \*\*不要假设键的顺序稳定。\*\*通知 body 中的字段顺序不保证与任何东西一致，这只在你打算自己对 body 做哈希或规范化时才有影响。你不需要这样做：对收到的原始字节验证 `X-SIGNATURE`。见 [Webhook 签名验证](/docs/zh/developer-docs/webhook/signature-verification)。

## 能给我每种事件类型一份完整的真实示例 body 吗？

按事件的示例维护在 [API Reference](/docs/api-reference/introduction) 而不是这里，因为每个 `result` 就是对应的查询响应，而 API Reference 由实时契约生成——这使它成为唯一不会漂移的副本。

把它当作 fixture 来源时的两个提醒：

* 已发布的参考会周期性重新生成，所以报文中存在的字段可能一段时间内在参考中缺失。`payMethodResponse` 内的卡键和订阅加购详情就是当前的例子：它们会被投递，却只被部分描述。把报文当作超集，让你的解析器容忍未知字段。
* \*\*能捕获真实报文时不要从文档构建 fixture。\*\*在沙箱中跑通流程并记录到达你端点的内容——那是你商户配置下、带你的支付方式的报文，也是你的处理程序真正要应对的。见 [Sandbox Simulation Limits FAQ](/docs/zh/support/sandbox-simulation-limits-faq)。

***


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.