StablePay 会通过 Webhook 将关键支付、退款、订阅和发票事件异步推送到你在商户后台配置的回调 URL。
本页汇总 Webhook 的事件类型、请求头、签名校验要求,以及近期新增的风控冻结状态适配说明。
在 Merchant Portal 中配置 Webhook
在你的服务端能够接收 Webhook 通知之前,需要先创建 API 店铺,并在 店铺和开发者 页面中配置回调地址。
第一步:创建 API 店铺
- 打开 店铺和开发者 页面。
- 点击【创建店铺】。
- 渠道类型选择 API。
- 按实际业务信息填写店铺名称和域名。
- 提交申请并等待 StablePay 审核。
第二步:店铺启用后配置 Webhook
当店铺状态变为【已启用】后:
- 返回 店铺和开发者 页面。
- 打开对应店铺的操作菜单。
- 点击【Webhook】。
- 填写你的 Webhook 回调 URL 并保存。
- 如有需要,也可以在同一操作菜单中点击【密钥】,创建服务端接入所需的 API Key 和 Secret Key。
- 如果有效期设置为
0天,表示该密钥长期有效。
建议使用可被 StablePay 公网访问的 HTTPS 服务端地址作为 Webhook 回调地址,并与浏览器页面或前端路由分开。请不要把前端页面 URL 直接作为 Webhook 目标地址。
事件类型
| 类别 | 事件类型 |
|---|
| 支付 | payment.completed、payment.failed、payment.expired、payment.cancelled |
| 退款 | refund.succeeded、refund.failed |
| 订阅 | subscription.created、subscription.trialing、subscription.active、subscription.past_due、subscription.canceled、subscription.updated、subscription.incomplete_expired |
| 发票 | invoice.created、invoice.paid、invoice.payment_failed |
Webhook 请求头
| 请求头 | 说明 |
|---|
Content-Type | 固定 application/json |
User-Agent | StablePay-Webhook/1.0 |
X-StablePay-Signature | 对 {timestamp}.{nonce}.{raw_body} 做 HMAC-SHA256 后得到的小写 hex 签名 |
X-StablePay-Timestamp | Unix 时间戳(秒) |
X-StablePay-Nonce | 随机字符串(16~64 字符),参与签名计算,每次通知唯一 |
X-StablePay-Event-Type | 事件类型,例如 payment.completed |
X-StablePay-Event-ID | 事件唯一 ID,用于幂等去重 |
签名校验
请使用原始请求体字节参与验签,不要在校验前先解析 JSON 再重新序列化。
签名串格式如下:
sign_payload = {timestamp} + "." + {nonce} + "." + {raw_body}
校验建议:
- 读取
X-StablePay-Signature、X-StablePay-Timestamp、X-StablePay-Nonce
- 校验时间戳与当前时间相差不超过 5 分钟
- 校验 nonce 长度为 16~64 字符
- 使用 Secret Key 对签名串做 HMAC-SHA256
- 使用恒定时间比较方式校验签名
签名校验代码示例可参考 集成前准备。
响应与重试
- 商户服务需在 30 秒内返回 HTTP
2xx
- 返回
429 或 5xx 会进入重试队列,其他 4xx 默认不重试
- 最多重试 10 次
- 建议使用
X-StablePay-Event-ID 或请求体中的 id 字段做幂等去重
payment.failed 与 frozen 状态
近期 StablePay 加强了风控能力。上线后,当支付订单触发高风险风控策略时,资金可能会被暂时冻结。这类场景通常涉及疑似黑灰产、异常交易或其他高风险行为,订单需要经过进一步审核后再确认后续处理结果。
在该机制上线后,StablePay 仍然会继续使用现有的 payment.failed webhook 事件类型 通知商户系统,但回调中的支付状态会新增:
这意味着 payment.failed 事件中的 data.object.status 现在至少有两种处理分支:
failed:普通支付失败,按原有失败逻辑处理
frozen:风控冻结,不应直接视为普通支付失败
不同渠道或集成来源下,业务扩展字段可能出现在 metadata 中,也可能以独立字段形式返回。请以 type、id、以及 data.object 中与你业务处理直接相关的核心状态字段为准。
frozen 示例回调
{
"id": "evt_1778818565572372433",
"data": {
"object": {
"amount": 1,
"status": "frozen",
"currency": "USDT",
"metadata": {
"source": "api",
"wc_order_id": "order_0b543c39"
},
"order_id": "order_0b543c39",
"session_id": "20260515110100101649130000272078",
"exchange_rate": ""
}
},
"type": "payment.failed",
"created_at": 1778818549
}
处理建议
当你收到 payment.failed 事件时,请额外判断 data.object.status:
- 当
status = "failed" 时,按原有支付失败逻辑处理
- 当
status = "frozen" 时,建议标记为“风控冻结”或“待风控审核”
对于 frozen 场景,不要直接按普通支付失败处理,也不建议自动执行以下动作:
事件样例
payment.completed
{
"id": "evt_1778835561972546443",
"type": "payment.completed",
"created_at": 1778835561,
"data": {
"object": {
"amount": 1,
"status": "completed",
"currency": "USDT",
"order_id": "order_56929d9f",
"session_id": "20260515110100101170210000082012",
"exchange_rate": "",
"source": "api"
}
}
}
payment.failed
{
"id": "evt_1778834854265555041",
"type": "payment.failed",
"created_at": 1778834851,
"data": {
"object": {
"amount": 1,
"status": "frozen",
"currency": "USDT",
"order_id": "order_31509b43",
"session_id": "20260515110100101170210000082010",
"exchange_rate": "",
"source": "api"
}
}
}
payment.expired
{
"id": "evt_1778836650899324281",
"type": "payment.expired",
"created_at": 1778836650,
"data": {
"object": {
"amount": 0,
"status": "expired",
"currency": "",
"order_id": "order_d3ff11a8",
"session_id": "20260515110100101170210000082011",
"exchange_rate": "",
"source": "api"
}
}
}
payment.cancelled
{
"id": "evt_1770864227443530013",
"type": "payment.cancelled",
"created_at": 1770864227,
"data": {
"object": {
"amount": 0,
"status": "canceled",
"currency": "",
"order_id": "order_29",
"session_id": "20260212110100101170210000028002",
"exchange_rate": "",
"source": "api"
}
}
}
相关页面