Skip to Content
Webhook概览

Webhook 概览

Webhook 是权威信号。浏览器回调 (onSuccess) 和仪表板视图只是 便利信息;Webhook 才是真相之源。

投递保证

  • 至少一次。 如果你的服务器未在超时内返回 2xx,单条事件最多会被 投递 6 次。处理函数请保持幂等 — 用 X-Delivery 去重。
  • 每个 HTTP 请求只投一条事件。 不做批处理。
  • 每个端点独立。 如果你注册了多个端点,每个端点都有独立的投递与 重试轨道。某个慢的商户 URL 不会拖累其他端点 — 每台主机都有自己的 断路器。
  • 签名。 每个负载都带 X-Signature 标头(在密钥轮换后的 24 小时 窗口期内,还会附带 X-Signature-Prev)。处理 body 之前先验证。 详见签名验证

可订阅的事件类型

事件触发条件
payment.settled链上转账达到该链的确认数。用此事件标记订单为已支付。
payment.failed一笔法币支付被 provider 明确拒绝(目前是 Stripe Webhook 上报失败)。为加密超时触发 — 那些以 checkout.expired 形式出现;短付的加密支付以 payment.underpaid 出现。
payment.underpaid资金已到达但低于订单总额(典型例:稳定币转账手续费从金额中扣除)。
payment.overpaid资金超过订单总额。多余部分被记录但不自动退款。
order.created新订单创建 — 通过你的 B2B API 调用或结账会话转换。
order.canceled订单转为取消。负载中的 data.reason 区分手动取消与 payment_timeout(worker 清扫的过期未付订单)。
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.sourceb2b / 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 是商户可选的备注。

仪表板从 GET /v1/webhooks/event-types 获取此列表,所以端点创建与 编辑表单始终匹配平台实际发出的事件。订阅平台不发出的事件会在创建时 被拒绝并附带清晰的错误信息。

测试事件不可订阅。 仪表板上每端点的 Send Test 按钮会把一个 webhook.test.ping 信封同步 POST 到那一个端点(绕过重试管道);而传统 的商户级”发送测试事件”通道则无论过滤器如何,都会把 webhook.test 信封扇出到每个活跃端点。两者都不出现在上面的目录里 — 你是凭借注册了 端点才会收到它们,而不是靠订阅。

只订阅你会处理的事件。每个端点有独立的事件过滤器;通配符 "*" 表示”所有事件,包括未来新增的”。订阅更少的事件可以让你的处理函数 更整洁,并且减少我们需要重试的表面积。

负载 + 标头

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": { /* 事件专属 */ } }

其他事件携带各自的字段集 — 在分事件文档落地之前,以 payment-service/internal/domain/events.go 里的发布器 struct 为准。 字段名稳定(lower snake_case);链上 tx hash 始终为 tx_hash (不是 transaction_hash)。

入站请求的标头

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(相同的值)。每次投递都会设置 — 沿用 Stripe / GitHub 的约定。
X-Timestamp尝试发送时的 Unix 秒。签入负载,使得抓到的 (body, X-Signature) 对不能被无限重放 — 时间戳超出你容差窗口的投递请拒绝。
X-SignatureHMAC-SHA256(secret, X-Timestamp + "." + raw_body)sha256=<hex>。详见签名验证
X-Signature-Prev同算法但用前一个密钥计算。仅在轮换后 24 小时窗口期内出现 — 让运行任一密钥的验证器在切换期间都能继续接受投递。窗口关闭后,此标头不再发送。

重试时间表

如果你的端点未在超时内返回 2xx,我们按以下时间表重试(时间戳相对 首次尝试):

尝试延迟累计
10s0s
2+1 分1m
3+5 分6m
4+15 分21m
5+1 小时1h 21m
6+6 小时7h 21m

第 6 次尝试失败后,投递移入死信,商户账户邮箱会收到通知。死信 事件可以从仪表板的 Developers → Webhooks → Delivery history 面板 自助重放,也可直接调用 POST /v1/webhooks/deliveries/:id/replay。 每次重放会创建一条新投递行,带自己的 X-Delivery — 审计链通过 parent_delivery_id 追溯回原事件,所以”重放的重试” 不会遮蔽源事件。

注册端点

商户仪表板 中:

  1. Developers → Webhooks+ Add endpoint
  2. 粘贴你的 URL — 仅 https://…(纯 HTTP 会被拒绝;创建表单还会 阻止 localhost、私有 IP 段、以及含用户信息的 URL)
  3. 选择要订阅的事件(或 * 表示全部)
  4. 选择环境 — testlive(各自有独立的密钥,二者永不交叉)
  5. 保存 → 仪表板仅显示一次签名密钥(whsec_…)。请保存到服务端; 下面两个功能会用到它。

每个商户每个环境最多可注册 10 个端点(例如:一个用于生产履约, 一个用于预生产镜像,一个用于 Slack 通知)。每个端点维护独立的重试 状态、密钥以及主机级断路器。

每个端点的生命周期操作

每张端点卡片的 ⋮ 菜单提供:

  • Edit — 修改 URL、描述或订阅列表。新 URL 会以与创建时相同的 https:///SSRF 规则重新校验。
  • Send Test — 用当前密钥同步地 POST 一个 webhook.test.ping 信封。 仪表板展示 HTTP 状态、延迟和你响应的前 512 字节。绕过 RMQ 管道, 结果立刻返回。
  • Rotate Secret — 生成新密钥。前一个密钥仍保持 24 小时 有效 (期间投递同时携带 X-SignatureX-Signature-Prev,这样无论 你的验证器使用哪个密钥,重新部署期间都能继续接受事件)。
  • Reveal Secret — 重新展示当前密钥。需要新的 2FA 验证并被记录到 审计日志;仅在你丢失副本且不能接受轮换时使用。
  • Enable / Disable — 在不丢失投递历史的前提下切换 is_active。 禁用端点仍留在仪表板,但不再接收新投递。
  • Delete — 永久删除。如果你以后可能再启用,使用 Disable。

处理函数小贴士

  1. 快速返回 2xx。 在做重活前先用 200 OK 应答 — 把履约推入 后台队列。每次尝试的超时是 10 秒;响应保持超过这个时间 会触发重试。该超时由平台侧设定,不可由商户配置 — 如果你的 处理函数确实需要更长时间,请联系支持团队。
  2. X-Delivery 去重(或 Idempotency-Key — 同一值)。 即便你返回了 2xx,上游代理也可能断连并触发重试;投递 ID 在同一 投递行的每次重试中保持稳定,所以这是正确的键。
  3. 容忍未知事件类型。 可能会出现新事件;请返回 200 + no-op, 而不是 4xx,否则你的重试队列会被塞满。
  4. 在业务逻辑旁边记录 X-Delivery 出问题时,这就是我们这边 与你那边的连接键。

下一步