Webhook 概览
Webhook 是权威信号。浏览器回调 (onSuccess) 和仪表板视图只是
便利信息;Webhook 才是真相之源。
投递保证
- 至少一次。 如果你的服务器未在超时内返回 2xx,单条事件最多会被
投递 6 次。处理函数请保持幂等 — 用
X-Delivery去重。 - 每个 HTTP 请求只投一条事件。 不做批处理。
- 每个端点独立。 如果你注册了多个端点,每个端点都有独立的投递与 重试轨道。某个慢的端点不会拖累其他端点。
- 签名。 每个负载都带
X-Signature标头(在密钥轮换后的 24 小时 窗口期内,还会附带X-Signature-Prev)。处理 body 之前先验证。 详见签名验证。
可订阅的事件类型
| 事件 | 触发条件 |
|---|---|
payment.settled | 链上转账达到该链的确认数。用此事件标记订单为已支付。 |
payment.failed | 一笔法币支付被支付 provider 拒绝。不为加密超时触发 — 那些以 checkout.expired 形式出现;短付的加密支付以 payment.underpaid 出现。 |
payment.underpaid | 资金已到达但低于订单总额(典型例:稳定币转账手续费从金额中扣除)。 |
payment.overpaid | 资金超过订单总额。多余部分被记录但不自动退款。 |
order.created | 新订单创建 — 通过你的 B2B API 调用或结账会话转换。 |
order.canceled | 订单转为取消。负载中的 data.reason 区分手动取消与 payment_timeout(未付订单超时)。 |
order.resolved | 一个 PARTIAL_PAID 订单被解决为 PAID — 商户接受了缺额。 |
order.reopened | 之前自动取消(canceled_reason=payment_timeout)的订单被商户重新打开。 |
checkout.created | 买家打开了订单的结账。 |
checkout.completed | 买家侧流程完成(不代表链上结算 — 那是 payment.settled 的事)。 |
checkout.expired | 买家放弃,会话 TTL 已过。 |
payment.refund.requested | 创建了一条退款记录 — 来源可能是商户发起的 API 调用,或客户提交的退款申请表单。 |
payment.refund.approved | 待审退款通过了你的审批流程。 |
payment.refund.rejected | 待审退款被驳回。 |
payment.refund.executed | 退款的链上转账确认,记录进入终态 executed。 |
refund_request.created | 退款申请 token 被铸造。data.source 是 b2b / dashboard / renewal。订阅可选 — 适合追踪每个订单当前活跃 token 的审计管道。 |
refund_request.renewal_requested | 买家在 token 过期后点击了”申请新链接”。强烈建议订阅 — 这是商户续期组件有新条目要处理的信号。 |
refund_request.renewed | 续期已批准,新 token 取代旧 token。data.old_token / data.new_token 构成审计链。 |
refund_request.canceled | 商户从仪表板把 token 翻转为 CANCELED(例如驳回续期、终止活跃链接)。幂等 — 只在首次迁移时发出。data.reason 是商户可选的备注。 |
计划中(即将推出)
即将推出。 这些事件属于定期账单和订阅功能,目前尚不可用。它们不在上方 可订阅的表格中,现在无法订阅。见定期账单。
| 计划中的事件 | 触发时机… |
|---|---|
subscription.created | 创建订阅时。 |
invoice.created | 创建某个计费周期的账单时。 |
invoice.paid | 账单已支付时。 |
subscription.past_due | 账单超过到期日仍未支付时。 |
subscription.canceled | 订阅被取消时。 |
仪表板的端点表单列出相同的事件。订阅不存在的事件会在你保存端点时 被拒绝。
测试事件不可订阅。 仪表板上每端点的 Send Test 按钮会立即向那一个
端点发送一个 webhook.test.ping 事件,不做重试。它不出现在上面的目录里:
你是因为注册了端点才会收到它,而不是因为订阅了它。
只订阅你会处理的事件。每个端点有独立的事件过滤器;通配符 "*"
表示”所有事件,包括未来新增的”。订阅更少的事件可以让你的处理函数
更简单,并且在你的端点出错时减少重试。
负载 + 标头
HTTP body 直接就是事件专属的 data 对象。 没有 Stripe 风格的外层
信封 — 事件类型、投递 ID、发出时间等字段都放在标头里。
对 payment.settled,body 形态如下:
{
"receipt_id": "rcp_…",
"order_id": "ord_…",
"payment_intent_id": "pin_…",
"checkout_session_id": "cst_…",
"merchant_id": "mer_…",
"customer_id": "cus_…",
"total": "49.00",
"currency": "USD",
"payment_method": "crypto",
"token": "USDC",
"network": "polygon",
"tx_hash": "0x…",
"deposit_address": "0x…",
"treasury_address": "0x…",
"amount_received": "49.00",
"confirmations": 5,
"metadata": { /* 事件专属 */ }
}其他事件携带各自的字段。字段名稳定(lower snake_case);链上交易哈希始终为 tx_hash。
tx_hash 是该网络自有格式的交易标识符(EVM 链上为 0x…;TRON、Solana、TON 上为原生哈希或签名)。对于 TRON、Solana 和 TON,买家直接向你的资金库钱包付款,因此 deposit_address 可能不存在;confirmations 遵循链与资产。
入站请求的标头
Content-Type: application/json
X-Event: payment.settled
X-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
X-Timestamp: 1729536000
X-Signature: sha256=9a8b7c…
X-Signature-Prev: sha256=fa31b2… (仅在轮换宽限窗口期内出现)| 标头 | 含义 |
|---|---|
X-Event | 事件类型(例如 payment.settled)。如果你想跳过 JSON 解析,可在代理层基于此路由。 |
X-Delivery | 标识投递行的 UUID。在同一 (event, endpoint) 对的所有重试间保持稳定 — 用作你的幂等键。 |
Idempotency-Key | 镜像 X-Delivery(相同的值)。每次投递都会设置。 |
X-Timestamp | 尝试发送时的 Unix 秒。签入负载,使得抓到的 (body, X-Signature) 对不能被无限重放 — 时间戳超出你容差窗口的投递请拒绝。 |
X-Signature | HMAC-SHA256(secret, X-Timestamp + "." + raw_body) 的 sha256=<hex>。详见签名验证。 |
X-Signature-Prev | 同算法但用前一个密钥计算。仅在轮换后 24 小时窗口期内出现 — 让运行任一密钥的验证器在切换期间都能继续接受投递。窗口关闭后,此标头不再发送。 |
重试时间表
如果你的端点未在超时内返回 2xx,我们按以下时间表重试(时间戳相对
首次尝试):
| 尝试 | 延迟 | 累计 |
|---|---|---|
| 1 | 0s | 0s |
| 2 | +1 分 | 1m |
| 3 | +5 分 | 6m |
| 4 | +15 分 | 21m |
| 5 | +1 小时 | 1h 21m |
| 6 | +6 小时 | 7h 21m |
第 6 次尝试失败后,该投递会被标记为失败,你的账户邮箱会收到通知。
你可以从仪表板的 Developers → Webhooks → Delivery history 面板
重放失败的事件。每次重放都是一条新投递,带自己的 X-Delivery。
注册端点
在商户仪表板 中:
- Developers → Webhooks → + Add endpoint
- 粘贴你的 URL — 仅
https://…(纯 HTTP 会被拒绝;创建表单还会 阻止localhost、私有 IP 段、以及含用户信息的 URL) - 选择要订阅的事件(或
*表示全部) - 选择环境 — test 或 live(各自有独立的密钥,二者永不交叉)
- 保存 → 仪表板仅显示一次签名密钥(
whsec_…)。请保存到服务端; 下面两个功能会用到它。
每个商户每个环境最多可注册 10 个端点(例如:一个用于生产履约, 一个用于预生产镜像,一个用于 Slack 通知)。每个端点有独立的重试状态和密钥。
每个端点的生命周期操作
每张端点卡片的 ⋮ 菜单提供:
- Edit — 修改 URL、描述或订阅列表。新 URL 会以与创建时相同的
https:///SSRF 规则重新校验。 - Send Test — 用当前密钥同步地 POST 一个
webhook.test.ping信封。 仪表板展示 HTTP 状态、延迟和你响应的前 512 字节。 测试 ping 不会重试,所以结果立刻返回。 - Rotate Secret — 生成新密钥。前一个密钥仍保持 24 小时 有效
(期间投递同时携带
X-Signature与X-Signature-Prev,这样无论 你的验证器使用哪个密钥,重新部署期间都能继续接受事件)。 - Reveal Secret — 重新展示当前密钥。需要新的 2FA 验证并被记录到 审计日志;仅在你丢失副本且不能接受轮换时使用。
- Enable / Disable — 在不丢失投递历史的前提下开启或关闭端点。 禁用端点仍留在仪表板,但不再接收新投递。
- Delete — 永久删除。如果你以后可能再启用,使用 Disable。
处理函数小贴士
- 快速返回 2xx。 在做重活前先用
200 OK应答 — 把履约转移到 后台任务。每次尝试的超时是 10 秒;响应保持超过这个时间 会触发重试。该超时由平台侧设定,不可由商户配置 — 如果你的 处理函数确实需要更长时间,请联系支持团队。 - 用
X-Delivery去重(或Idempotency-Key— 同一值)。 即便你返回了 2xx,上游代理也可能断连并触发重试;投递 ID 在同一 投递行的每次重试中保持稳定,所以这是正确的键。 - 容忍未知事件类型。 可能会出现新事件;请返回 200 + no-op, 而不是 4xx,否则这些投递会不断重试。
- 在业务逻辑旁边记录
X-Delivery。 出问题时,这就是我们这边 与你那边的连接键。