<!-- Source: https://docs.infraio.xyz/zh-CN/webhooks/overview -->
<!-- Last updated: 2026-10-04 -->

# Webhook 概览

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

## 投递保证

- **至少一次。** 如果你的服务器未在超时内返回 2xx,单条事件最多会被
  投递 **6 次**。处理函数请保持幂等 — 用 `X-Delivery` 去重。
- **每个 HTTP 请求只投一条事件。** 不做批处理。
- **每个端点独立。** 如果你注册了多个端点,每个端点都有独立的投递与
  重试轨道。某个慢的端点不会拖累其他端点。
- **签名。** 每个负载都带 `X-Signature` 标头(在密钥轮换后的 24 小时
  窗口期内,还会附带 `X-Signature-Prev`)。处理 body 之前先验证。
  详见[签名验证](https://docs.infraio.xyz/zh-CN/webhooks/signature-verification)。

## 可订阅的事件类型

| 事件 | 触发条件 |
| --- | --- |
| `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` 是商户可选的备注。 |

### 计划中(即将推出)

> **Note:**
>
> **即将推出。** 这些事件属于定期账单和订阅功能,目前尚不可用。它们**不在**上方
> 可订阅的表格中,现在无法订阅。见[定期账单](https://docs.infraio.xyz/zh-CN/guides/recurring-invoices)。

| 计划中的事件 | 触发时机… |
| --- | --- |
| `subscription.created` | 创建订阅时。 |
| `invoice.created` | 创建某个计费周期的账单时。 |
| `invoice.paid` | 账单已支付时。 |
| `subscription.past_due` | 账单超过到期日仍未支付时。 |
| `subscription.canceled` | 订阅被取消时。 |

仪表板的端点表单列出相同的事件。订阅不存在的事件会在你保存端点时
被拒绝。

> **Note:**
>
> **测试事件不可订阅。** 仪表板上每端点的 **Send Test** 按钮会立即向那一个
> 端点发送一个 `webhook.test.ping` 事件,不做重试。它不出现在上面的目录里:
> 你是因为注册了端点才会收到它,而不是因为订阅了它。

> **Note:**
>
> 只订阅你会处理的事件。每个端点有独立的事件过滤器;通配符 `"*"`
> 表示"所有事件,包括未来新增的"。订阅更少的事件可以让你的处理函数
> 更简单,并且在你的端点出错时减少重试。

## 负载 + 标头

**HTTP body 直接就是事件专属的 data 对象。** 没有 Stripe 风格的外层
信封 — 事件类型、投递 ID、发出时间等字段都放在**标头**里。
对 `payment.settled`,body 形态如下:

```json
{
  "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` 遵循[链与资产](https://docs.infraio.xyz/zh-CN/concepts/chains)。

### 入站请求的标头

```http
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>`。详见[签名验证](https://docs.infraio.xyz/zh-CN/webhooks/signature-verification)。 |
| `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`。

## 注册端点

在[商户仪表板](https://app.infraio.xyz)中:

1. **Developers → Webhooks** → **+ Add endpoint**
2. 粘贴你的 URL — 仅 `https://…`(纯 HTTP 会被拒绝;创建表单还会
   阻止 `localhost`、私有 IP 段、以及含用户信息的 URL)
3. 选择要订阅的事件(或 `*` 表示全部)
4. 选择环境 — **test** 或 **live**(各自有独立的密钥,二者永不交叉)
5. 保存 → 仪表板**仅显示一次**签名密钥(`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。

## 处理函数小贴士

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

## 下一步

- [签名验证](https://docs.infraio.xyz/zh-CN/webhooks/signature-verification) — 精确算法 +
  防重放模式。
- [概念 → 会话](https://docs.infraio.xyz/zh-CN/concepts/sessions) — 每个事件触发时会话
  处于什么状态。
