通知介绍与配置
平台会向商户 Portal 中独立配置的 HTTPS Webhook Endpoint 投递已签名的订单最终状态事件。Endpoint 不绑定 API Key, 该地址不需要在每笔代收或代付创建请求中传入。请求 Body 使用 UTF-8 JSON,并固定发送 Content-Type: application/json。 这是平台主动发出的 Webhook 投递,不是商户调用的普通 API 接口。
配置与密钥生命周期
- 商户可以创建多个 Endpoint,并为每个 Endpoint 选择事件类型;同一事件类型可以同时投递到多个 Endpoint。新 Endpoint 默认停用。
- 创建 Endpoint 时,平台生成 Webhook Secret 和 Webhook Secret ID,当前 Secret 明文显示 60 秒。具备 Webhook Secret 查看权限的商户管理员每次通过新的 Google 验证后可再次查看当前版本;历史、已吊销、已归档或运营暂停版本不能查看。
- 商户把 Secret 保存到服务端密钥管理系统,并部署能在处理事件前完成原始 Body 验签的接收端。
- 接收端具备可靠接收能力后再独立启用该 Endpoint。接收端示例建议返回 HTTP
200 OK; 平台会将任意2xx响应视为接收成功。 - 修改 URL 会自动停用 Endpoint,并将待投递记录标记为跳过;新接收端审核完成后再启用。轮换 Secret 前只需停用该 Endpoint,API Key 状态与其无关。
- 初次投递和自动重试使用投递记录创建时保存的 URL 与 Secret 版本;商户或运营人工补发使用 Endpoint 当前 URL 与当前 Secret。接收端在最长自动重试周期内应保留旧 Secret ID 映射,并严格按
Webhook-Secret-Id选择。
商户后台可查看成功、失败和重试中的投递记录,并可在授权后单条补发;系统也会自动重试符合条件的失败投递。
请求 Header
这组 Header 只用于平台调用商户 Webhook 接收地址的通知投递,和商户调用普通 API 时发送的请求 Header 是两套规则。
| Header | 示例 | 描述 |
|---|---|---|
Content-Type | application/json | 表示本次 Webhook Body 是 UTF-8 JSON;不参与 Webhook 验签原文。 |
Request-Id | req_fixture_000000000000000000000001 | 本次投递请求追踪号,用于排障和日志关联。 |
Webhook-Secret-Id | whsecid_fixture_001 | 本次投递选用的 Webhook Secret 标识;用于在验签前选择本地保存的 Webhook Secret。商户在 Portal 生成或轮换 Webhook Secret 时获取并保存。 |
Timestamp | 1780272000 | 本次投递的 Unix 秒级时间戳文本,重试时可能变化。 |
Nonce | nonce_fixture_webhook_001 | 本次投递随机串,重试时可能变化。 |
Signature | v1=<64 lowercase hex> | 对时间戳、随机串和原始 Body 计算的版本化 HMAC-SHA256 签名。 |
Header 名称大小写不敏感;文档统一展示无品牌前缀的规范 Header 名,调用时不要自行添加产品、公司或品牌前缀。
HTTP 示例
{merchantWebhookUrl} 是商户在后台配置的完整 HTTPS 通知接收地址,域名和路径均由商户自定义。 代收、代付通知可共用同一个 Endpoint,由 Body eventType 区分,也可以通过不同事件订阅路由到不同 Endpoint;同一事件类型也可由多个 Endpoint 订阅。
示例地址:https://merchant.example.com/。
POST {merchantWebhookUrl}
Content-Type: application/json
Request-Id: req_fixture_000000000000000000000001
Webhook-Secret-Id: whsecid_fixture_001
Timestamp: 1780272000
Nonce: nonce_fixture_webhook_001
Signature: v1=<64 lowercase hex>
{
"eventId": "evt_fixture_001",
"eventType": "PAYMENT_SUCCEEDED",
"merchantNo": "MCH_FIXTURE_001",
"order": {
"platOrderNo": "PHI112606010000000001001",
"merchantOrderNo": "DEMO_PAY_202606010001",
"status": "SUCCEEDED",
"productCode": "PH_PHP_PAYMENT_QRPH_GCASH",
"country": "PH",
"amount": {
"value": "100.50",
"currency": "PHP"
},
"referenceNo": "REF-20260601-0001",
"customData": {
"cartId": "CART-10001",
"customerRef": "CUST-90001"
},
"createTime": 1780272000,
"updateTime": 1780275600
}
}签名校验
签名原文固定三行,每一行后都必须追加一个换行字节 \n,包括最后一行:
signingPayload = timestamp + "\n" + nonce + "\n" + rawBody + "\n"
signature = "v1=" + lowercase_hex(HMAC-SHA256(webhookSecret, signingPayload))必须在 JSON 解析、格式化或日志转换之前保留原始 Body。先按接收方配置的时间窗口 检查时间戳,再根据 Webhook-Secret-Id 精确选择本地保存的 Secret。 该 Header 只能作为本地白名单的选择条件,不能直接用于拼接配置项。Secret ID 缺失或未知时 必须直接拒绝处理,禁止降级为逐个尝试全部历史 Secret。
使用常量时间比较期望签名与收到的签名。只有时间戳和签名都校验成功后,才解析并处理 JSON。
WebhookEvent 字段
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
eventId | string | 是 | 用于去重的稳定事件 ID。 |
eventType | string | 是 | 用于路由业务处理的稳定最终状态事件类型。 |
merchantNo | string | 是 | 本次通知订单所属商户号。 |
order | object | 是 | 本次通知携带的订单最终状态数据。 |
order.platOrderNo | string | 是 | 核心交易系统的真实平台订单号,用于技术排障和对账;应作为不透明值使用,不根据格式猜测订单类型。 |
order.merchantOrderNo | string | 是 | 商户侧订单号,用于业务状态更新。 |
order.status | string | 是 | 当前订单最终状态。 |
order.failReason | string | 否 | 面向商户运营展示或排障的失败/取消原因。order.status 为 FAILED 或 CANCELED 时返回;SUCCEEDED 时不返回。 |
order.productCode | string | 是 | 本笔订单使用的产品编码。 |
order.country | string | 是 | ISO 3166-1 alpha-2 两位大写国家码。 |
order.amount | object | 是 | 订单金额对象。 |
order.amount.value | string | 是 | 金额值:交易金额,主币种单位,小数精度以 amount.currency 和所选 productCode 为准。 |
order.amount.currency | string | 是 | 币种编码:交易币种,使用 ISO 4217 三位大写编码。见支持国家与币种。 |
order.referenceNo | string | 否 | 上游支付网络、付款机构或收款机构返回的参考号;用于将付款凭证或收款核实结果关联到订单。 |
order.customData | object | 否 | 首次受理创建请求保存的非敏感商户自定义键值;幂等重复请求不会合并或覆盖。 |
order.recipientSummary | object | 否 | 脱敏收款账户摘要;代付通知可在有值时返回。 |
order.recipientSummary.bankCode | string | 返回 recipientSummary 时必填 | 收款银行编码。 |
order.recipientSummary.accountName | string | 返回 recipientSummary 时必填 | 收款户名。 |
order.recipientSummary.accountNoLast4 | string | 返回 recipientSummary 时必填 | 收款账号后四位。 |
order.createTime | timestamp | 是 | 订单创建时的 Unix 秒级时间戳,JSON number。 |
order.updateTime | timestamp | 是 | 当前通知订单状态生效时的 Unix 秒级时间戳,JSON number;同一个事件重试时该值保持不变。 |
Header 中的 Timestamp 表示本次 HTTP 投递时间,重试时可能变化,并以文本发送。Body 中的 order.updateTime 表示本次通知订单状态的生效时间,以 JSON number 返回,同一事件重试时保持稳定。
Body eventType 使用固定枚举:
eventType | 含义 |
|---|---|
PAYMENT_SUCCEEDED | 代收订单进入成功最终状态。 |
PAYMENT_CANCELED | 代收订单在成功前已过期或关闭。 |
PAYOUT_SUCCEEDED | 代付订单进入成功最终状态。 |
PAYOUT_FAILED | 代付订单进入失败最终状态。 |
order.referenceNo 不是商户订单号、平台订单号或渠道订单号;它只用于付款凭证核实、收款机构核实和人工排障,不作为普通查单或幂等依据。
投递处理要求
- 先验签,再按 Body
eventId幂等去重并可靠落库,然后尽快返回任意 HTTP2xx响应;无需返回业务数据。 - 平台会将任意
2xx响应视为接收成功,并停止该次通知后续重试。即使订单已处理过,商户也应在幂等处理后返回2xx,避免重复投递。 - 非
2xx响应、接收端超时、连接失败或 TLS 失败会触发自动重试。默认重试窗口为 24 小时,重试节奏约为 1m、2m、4m、8m、16m、32m,之后约每 1h 重试一次。实际投递时间可能有浮动,不承诺秒级准确;超过重试窗口仍失败的事件进入人工补偿。 - 按 Body
eventId去重;Request-Id只代表一次投递请求,仅用于排障和日志关联,不能作为业务去重依据。 - 已接收事件应异步处理,并保证业务消费幂等。Webhook 不保证严格顺序;本地状态推进不明确时,按
merchantOrderNo查单兜底。 - 重试可能携带新的
Request-Id、投递时间戳文本、随机串和签名;BodyeventId和order.updateTime保持原事件值。 Webhook-Secret-Id、时间戳或签名校验失败时必须直接拒绝处理;验签成功前不要解析或处理 JSON Payload。
机器可读 Schema 位于 OpenAPI Schema 的 components.schemas.WebhookEvent。可在 代收订单通知 和 代付订单通知 查看渲染后的 Schema 和 Java 验签示例。自行实现 Webhook 验签时,应使用提供的 Webhook 签名测试向量完成校验。
