## 7. Webhook（订单、订阅与卡回调）

当订单、订阅或卡发生变化，或者生成卡片授权挑战时，Infini 会向商户预先配置的 Webhook 地址发送 HTTP POST 请求。通知中包含履约、服务开通、卡片业务处理、对账及客服处理所需的最新状态。

### 7.1 可订阅事件与事件类型

在商户后台可以配置订阅类型，不同订阅类型对应的事件如下：

**订单事件：**

- 订阅 `order.create` → 会收到 `order.create`
- 按需订阅细粒度订单更新事件：
  - `order.processing`
  - `order.completed`
  - `order.expired`
  - `order.late_payment`
- 仍订阅旧版 `order.update` 的存量端点，会继续收到上述细粒度订单更新事件（以及适用时的 `payment.failed`）。新建订阅请使用细粒度事件名。


**订单事件类型说明：**

- order.create：订单创建成功，进入待支付状态。
- order.processing：订单进入处理中（收到部分支付或存在确认中的链上交易）。
- order.completed：订单金额已满足，订单状态为 paid。
- order.expired：订单在有效期内未完成支付，已过期。
- order.late_payment：订单过期后收到付款。


**自动退款事件：**

- 订阅 `auto_refund.completed` → 会收到 `auto_refund.completed`
- 订阅 `auto_refund.unclaimed` → 会收到 `auto_refund.unclaimed`


**自动退款事件类型说明：**

- auto_refund.completed：自动退款已成功链上打出。
- auto_refund.unclaimed：付款人未在确认期内提交退款地址，金额转入商户结算。


触发条件见 [自动退款](/zh/docs/en/12-automatic-refund)。

**订阅事件：**

- 订阅 subscription.update → 会收到：
  - subscription.update
- 订阅 subscription.cancel → 会收到：
  - subscription.cancel


**订阅事件类型说明：**

- subscription.update：订阅状态更新（如首次支付完成后激活、续费支付成功后计费周期更新）。
- subscription.cancel：订阅已取消（商户 API 取消、用户退订、或系统因未支付自动取消）。


**卡事件：**

- 订阅 `card.status_change` → 会收到：
  - `card.status_change`
- 订阅 `card.transaction` → 会收到：
  - `card.transaction`
- 订阅 `card.challenge` → 会收到：
  - `card.challenge`


**卡事件类型说明：**

- `card.status_change`：卡当前生命周期快照，包括成功激活、冻结、解冻和最终删除。
- `card.transaction`：卡交易更新，包括开卡、充值、赎回、消费、退款、冲正、清算及当前交易状态。
- `card.challenge`：卡交易的一次性授权码挑战。该事件必须显式订阅，并且在终态投递后不能从开发者后台手动重发。


> 说明：


- 新建 Webhook 配置时，请使用精确事件名订阅。当前 Webhook 管理 API 不支持通配符订阅。
- 个人卡活动不会触发 Card Webhook。
- `card.challenge` 仅会在 `expires_at` 之前投递；请立即处理，并在安全落库后再返回 HTTP 200。


#### 7.1.1 测试工具

如需查看收到的 Webhook 请求，可使用 [Webhook Cool](https://webhook.cool/)，它会提供一个唯一的 HTTPS URL 用于接收事件。

如需在 Sandbox 环境触发真实的卡交易事件，可使用 [Card Simulator](https://card-simulator.infini.money/)。输入卡 UUID 后，可以选择以下场景：

- 消费授权
- 授权失败
- 全部或部分入账
- 授权撤销
- 已入账交易退款


执行模拟前，请先配置 Sandbox Webhook Endpoint，并显式订阅 `card.transaction`。模拟器会触发真实的 Sandbox 交易处理链路，但不会展示商户收到的回调，也不会模拟 `card.status_change` 或 `card.challenge`。

### 7.2 Webhook Payload 字段说明

#### 订单 Webhook Payload 字段

Webhook 请求的 body 为 JSON 格式，字段包括：

- event：事件类型（如 order.create）
- order_id：订单唯一标识
- client_reference：商户侧订单号（即 client_reference）
- amount：订单应付金额（法币金额）
- currency：订单币种（如 USD）
- status：订单状态：
  - pending
  - processing
  - paid
  - partial_paid
  - expired
- amount_confirming：确认中金额（链上交易存在但尚未达到确认要求）
- amount_confirmed：已确认金额（链上确认完成）
- created_at：订单创建时间（Unix 时间戳，秒）
- updated_at：订单最近一次更新时间（Unix 时间戳，秒）
- exception_tags（如果有）：订单异常标签数组（如 ["underpaid", "late"]），详见「业务核心概念」章节


> 说明：


- amount_confirmed + amount_confirming 反映链上已识别到的总支付金额。
- 订单状态与是否过期、已确认/确认中金额共同构成订单当前语义。


#### 自动退款 Webhook Payload 字段

自动退款 Webhook 的 body 包含订单上下文和 `auto_refund` 对象：

- event：`auto_refund.completed` 或 `auto_refund.unclaimed`
- event_id：稳定事件 ID，用于幂等
- order_id / client_reference / amount / currency / status
- amount_confirmed / amount_confirming
- refunded_amount：仅 `auto_refund.completed` 包含（原币退款本金）
- payments：本退款组覆盖的来源 Payment
- auto_refund：
  - id：自动退款单 ID
  - original_payment_ids：来源 Payment ID 列表
  - reason：`underpayment` / `overpayment` / `late_payment`
  - status：`completed` 或 `unclaimed`
  - currency / network
  - refund_amount：计划退款本金（原币）
  - gas_fee：从退款本金中扣除的 Gas
  - received_amount：扣除 Gas 后付款人实际收到金额
  - refund_address / tx_hash：链上退款完成时有值；未认领时为 null
  - claim_period_days / claim_deadline：仅 `auto_refund.unclaimed` 包含
  - created_at / completed_at：仅 `auto_refund.completed` 包含


#### 订阅 Webhook Payload 字段

订阅 Webhook 请求的 body 为 JSON 格式，字段包括：

- event：事件类型（`subscription.update` 或 `subscription.cancel`）
- subscription_id：系统生成的订阅 ID
- merchant_sub_id：商户自定义订阅 ID
- plan_name：订阅计划名称
- trigger_method：扣费触发方式（如 `invoice`）
- status：订阅状态：
  - pending
  - active
  - canceled
- currency：订阅币种（如 USD）
- amount：每期扣款金额
- interval_unit：扣款周期单位（`DAY` 或 `MONTH`）
- interval_count：每个扣款周期的间隔数量
- payer_email：付款人邮箱
- current_period_start：当前计费周期开始时间（Unix 时间戳，秒）
- current_period_end：当前计费周期结束时间（Unix 时间戳，秒）
- subscription_end_at：订阅终止时间（Unix 时间戳，秒）
- next_invoice_at：下次发送 Invoice 的时间（Unix 时间戳，秒）
- cancel_reason：取消原因（仅订阅被取消时返回）
- canceled_at：取消时间（Unix 时间戳，秒；仅订阅被取消时返回）
- created_at：订阅创建时间（Unix 时间戳，秒）
- updated_at：订阅最近一次更新时间（Unix 时间戳，秒）


#### Card Webhook 公共 Envelope

Card Webhook 使用带版本号的公共 Envelope：

- `id`：Webhook 投递 ID，也会出现在 `X-Webhook-Event-Id`
- `event`：事件类型，例如 `card.status_change`
- `version`：当前公共 Envelope 版本，当前为 `1`
- `occurred_at`：事件发生时间（Unix 时间戳，秒）
- `data`：事件对应的具体 payload


订单与订阅 Webhook 继续沿用现有的扁平 JSON payload 结构，不会带 `version` / `data` 包装层。

#### 卡状态变更 Payload（`card.status_change`）

`data` 对象包含：

- `card.card_id`
- `card.alias`（如有）
- `card.last_four`（如有）
- `card.status`
- `card.currency`


> 说明：


- `card.status` 可能为 `init`、`pending`、`active`、`suspend` 或 `deleted`。
- 开卡成功通过 `card.status_change` 且 `card.status: active` 表示，不会额外发送 `card.activated` 事件。
- `pending_delete` 属于内部删除过渡状态，不会单独发送 `card.status_change` Webhook。删除流程中请等待 `deleted`，或通过 `GET /v2/cards/status` 主动查询。


示例：

```json
{
  "id": "b7ef2c62-6177-4ea8-84ec-3080f2db58f0",
  "event": "card.status_change",
  "version": 1,
  "occurred_at": 1763513200,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    }
  }
}
```

#### 卡交易 Payload（`card.transaction`）

`data` 对象包含：

- `card`
- `transaction_id`
- `related_transaction_id`（退款 / 冲正时如果可用）
- `type`：`apply_card`、`top_up`、`redeem`、`consume`、`refund`、`reversal` 或 `clearing`
- `status`：`pending`、`authorized`、`completed` 或 `failed`
- `amount`
- `fee`
- `currency`
- `direction`：`debit`、`credit` 或 `none`
- `transaction_amount` 与 `transaction_currency`（渠道返回原始交易金额与币种时）
- `merchant`（如渠道返回商户信息）
- `transaction_at`
- `settled_at`
- `failure`（仅失败交易返回）
- `auth_settle_adjustment`（已完成的 `consume` 且存在授权上下文时返回）


所有金额字段均为十进制字符串。渠道未返回的可选字段会被省略。

**交易类型：**

| `type` | 含义 |
|  --- | --- |
| `apply_card` | 开卡申请或发卡账务交易 |
| `top_up` | 向卡片充值 |
| `redeem` | 从卡片赎回资金 |
| `consume` | 商户消费授权或入账 |
| `refund` | 已入账消费退款 |
| `reversal` | 撤销授权并释放预扣资金 |
| `clearing` | 卡商清算交易或调整 |


**交易状态：**

| `status` | 含义 |
|  --- | --- |
| `pending` | 交易已受理，但尚未授权或完成 |
| `authorized` | 消费授权成功，资金已预扣 |
| `completed` | 交易已进入最终成功状态 |
| `failed` | 交易失败；`direction` 为 `none`，并包含 `failure` |


同一笔交易可能产生多次 `card.transaction` Webhook。例如消费通常会从 `authorized` 变为 `completed`。每次投递的顶层 `id` 和 `X-Webhook-Event-Id` 都不同，请使用 `data.transaction_id` 关联状态更新。退款和冲正能够关联原消费交易时，`related_transaction_id` 会返回原消费交易 ID。

对于已完成的消费，`auth_settle_adjustment` 用于描述授权时预扣金额与最终入账金额之间的变化：

```
authorized_total = authorized_amount + authorized_fee
settled_total    = settled_amount + settled_fee
signed_delta     = authorized_total - settled_total
balance_delta    = balance_after - balance_before = signed_delta
```

- `direction: release`：授权总额大于入账总额，差额已释放。
- `direction: additional_debit`：入账总额大于授权总额，差额已补扣。
- `direction: none`：授权总额与入账总额相同。


**消费授权成功：**

```json
{
  "id": "f72f6cd7-1f38-5e96-b60f-f54e56c8db63",
  "event": "card.transaction",
  "version": 1,
  "occurred_at": 1786417200,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    },
    "transaction_id": "8c1db7df-3d4e-44de-b6b3-5ed451809d79",
    "type": "consume",
    "status": "authorized",
    "amount": "10",
    "fee": "0",
    "transaction_amount": "10",
    "currency": "USD",
    "transaction_currency": "USD",
    "direction": "debit",
    "merchant": {
      "name": "Amazon"
    },
    "transaction_at": 1786417200
  }
}
```

**消费入账金额小于授权金额：**

```json
{
  "id": "54b1090a-307e-5ec6-9adb-e877415222fd",
  "event": "card.transaction",
  "version": 1,
  "occurred_at": 1786417260,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    },
    "transaction_id": "8c1db7df-3d4e-44de-b6b3-5ed451809d79",
    "type": "consume",
    "status": "completed",
    "amount": "8",
    "fee": "0",
    "transaction_amount": "8",
    "currency": "USD",
    "transaction_currency": "USD",
    "direction": "debit",
    "merchant": {
      "name": "Amazon"
    },
    "transaction_at": 1786417200,
    "settled_at": 1786417260,
    "auth_settle_adjustment": {
      "authorized_amount": "10",
      "authorized_fee": "0",
      "settled_amount": "8",
      "settled_fee": "0",
      "signed_delta": "2",
      "direction": "release",
      "amount": "2",
      "balance_delta": "2",
      "balance_before": "90",
      "balance_after": "92"
    }
  }
}
```

**消费授权失败：**

```json
{
  "id": "1ae7c21c-71b4-5fd8-9f12-2a44f94bab41",
  "event": "card.transaction",
  "version": 1,
  "occurred_at": 1786417320,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    },
    "transaction_id": "f14c387d-02d1-41b8-aebb-1c9fda914385",
    "type": "consume",
    "status": "failed",
    "amount": "1000",
    "fee": "0",
    "transaction_amount": "1000",
    "currency": "USD",
    "transaction_currency": "USD",
    "direction": "none",
    "merchant": {
      "name": "Amazon"
    },
    "transaction_at": 1786417320,
    "failure": {
      "reason": "Insufficient balance"
    }
  }
}
```

**消费授权撤销：**

```json
{
  "id": "b3ab57ae-18f9-5ab7-8338-1202ea946994",
  "event": "card.transaction",
  "version": 1,
  "occurred_at": 1786417380,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    },
    "transaction_id": "7b9bb337-27f6-4d2a-b474-f91cc26a5e4c",
    "related_transaction_id": "8c1db7df-3d4e-44de-b6b3-5ed451809d79",
    "type": "reversal",
    "status": "completed",
    "amount": "10",
    "fee": "0",
    "currency": "USD",
    "direction": "credit",
    "transaction_at": 1786417380,
    "settled_at": 1786417380
  }
}
```

**已入账消费退款：**

```json
{
  "id": "ebc55ba6-5607-516e-902c-9400ec44c5bf",
  "event": "card.transaction",
  "version": 1,
  "occurred_at": 1786417440,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    },
    "transaction_id": "27dfc395-f703-4b16-90ca-f1cb9d3f1ec8",
    "related_transaction_id": "8c1db7df-3d4e-44de-b6b3-5ed451809d79",
    "type": "refund",
    "status": "completed",
    "amount": "8",
    "fee": "0",
    "currency": "USD",
    "direction": "credit",
    "transaction_at": 1786417440,
    "settled_at": 1786417440
  }
}
```

#### 卡挑战 Payload（`card.challenge`）

`data` 对象包含：

- `card`
- `challenge_id`
- `challenge_type`（当前为 `authorization_code`）
- `challenge`
- `expires_at`


渠道侧专用字段，例如 provider webhook ID、provider transaction ID、商户路由 ID、加密密文、PAN、CVV，不会出现在公开 payload 中。

示例：

```json
{
  "id": "12e7c9c8-598b-4dc5-b0c2-d7b6b8c3d8a4",
  "event": "card.challenge",
  "version": 1,
  "occurred_at": 1763513400,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    },
    "challenge_id": "challenge-1",
    "challenge_type": "authorization_code",
    "challenge": "123456",
    "expires_at": 1763513700
  }
}
```

### 7.3 Webhook 请求 Headers

Infini 在发送 Webhook 时，会附带以下 HTTP Header 用于安全校验与幂等处理：

- Content-Type: application/json
- X-Webhook-Timestamp：Unix 时间戳（秒）
- X-Webhook-Event-Id：事件唯一标识，用于幂等去重
- X-Webhook-Signature-Version：签名版本，当前值为 `v1`
- X-Webhook-Signature：HMAC-SHA256 签名，用于商户侧验签


> 建议商户使用 X-Webhook-Event-Id 做幂等处理，避免重复消费同一事件。


Card Webhook 始终带签名，并且要求存在可用的 Webhook Secret。解析 JSON 前，请使用未经任何修改的原始请求 Body 验签。

### 7.4 Webhook 示例 Payload

以下示例展示典型场景下的 Webhook 内容。

#### 7.4.1 场景 1：订单创建（order.created）

订单创建后，状态为 pending，等待用户付款。

```json
{
  "event": "order.create",
  "order_id": "10290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "pending",
  "amount_confirmed": "0",
  "amount_confirming": "0",
  "created_at": 1763512195,
  "updated_at": 1763512195
}
```

#### 7.4.2 场景 2：订单处理中（收到款项在确认中，order.processing）

收到付款，但交易仍在区块链确认中。

```json
{
  "event": "order.processing",
  "order_id": "20290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "processing",
  "amount_confirmed": "0",
  "amount_confirming": "0.5",
  "created_at": 1763512349,
  "updated_at": 1763512403
}
```

#### 7.4.3 场景 3：订单处理中（收到部分款项已确认，order.processing）

部分付款已经在区块链上确认。

```json
{
  "event": "order.processing",
  "order_id": "20290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "processing",
  "amount_confirmed": "0.5",
  "amount_confirming": "0",
  "created_at": 1763512349,
  "updated_at": 1763512453
}
```

#### 7.4.4 场景 4：订单完成（收到完整付款，order.completed）

收到足额付款并确认，订单完成。

```json
{
  "event": "order.completed",
  "order_id": "20290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "paid",
  "amount_confirmed": "1",
  "amount_confirming": "0",
  "created_at": 1763512349,
  "updated_at": 1763512573,
  "payments": [
    {
      "id": "pay_9c1e2f3a4b5c",
      "method": "crypto_transfer",
      "status": "paid",
      "currency": "USD",
      "amount": "1",
      "network": "ETH",
      "tx_hash": "0xabc123def456...",
      "paid_amount": "1.000000"
    }
  ]
}
```

#### 7.4.5 场景 5：订单过期（完全未付款，order.expired）

订单超时未收到任何付款。

```json
{
  "event": "order.expired",
  "order_id": "10290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "expired",
  "amount_confirmed": "0",
  "amount_confirming": "0",
  "created_at": 1763512195,
  "updated_at": 1763512255
}
```

#### 7.4.6 场景 6：订单过期（收到部分款项，order.expired + partial_paid）

订单超时但收到了部分付款，未达到订单金额。

```json
{
  "event": "order.expired",
  "order_id": "60290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "partial_paid",
  "amount_confirmed": "0.5",
  "amount_confirming": "0",
  "created_at": 1763514565,
  "updated_at": 1763514765
}
```

#### 7.4.7 场景 7：订单过期后收到付款（晚到付款，order.late_payment）

订单过期后 24 小时内收到付款。

```json
{
  "event": "order.late_payment",
  "order_id": "30290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "expired",
  "amount_confirmed": "1",
  "amount_confirming": "0",
  "created_at": 1763512622,
  "updated_at": 1763512815
}
```

> 提示：


- late_payment 场景下，订单状态仍为 expired，但 amount_confirmed 已达到订单金额，商户可根据业务策略决定是否发货或退款。
- 建议结合异常标签（如 late、underpaid、overpaid）进行业务决策。
- 若已开启自动退款，符合条件的晚付 / 少付 / 多付后续还可能产生 `auto_refund.*` 事件。详见 [自动退款](/zh/docs/en/12-automatic-refund)。


#### 7.4.8 场景 8：自动退款完成（auto_refund.completed）

```json
{
  "event": "auto_refund.completed",
  "event_id": "evt_xxx",
  "order_id": "20290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "ORDER-10001",
  "amount": "100",
  "currency": "USD",
  "status": "partial_paid",
  "amount_confirmed": "50",
  "amount_confirming": "0",
  "refunded_amount": "50",
  "created_at": 1763512349,
  "updated_at": 1763513573,
  "payments": [
    {
      "id": "pay_8b9c0d1e2f3a",
      "method": "Crypto Transfer",
      "status": "paid",
      "currency": "USD",
      "amount": "30",
      "paid_currency": "USDT",
      "paid_amount": "30",
      "network": "TRON",
      "tx_hash": "4e82a9b1...",
      "created_at": 1763512400,
      "completed_at": 1763512500
    }
  ],
  "auto_refund": {
    "id": "ref_7f8e9d0c1b2a",
    "original_payment_ids": ["pay_8b9c0d1e2f3a"],
    "reason": "underpayment",
    "status": "completed",
    "currency": "USDT",
    "refund_amount": "50",
    "gas_fee": "1",
    "received_amount": "49",
    "network": "TRON",
    "refund_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "tx_hash": "f91d38ab...",
    "created_at": 1763513400,
    "completed_at": 1763513573
  }
}
```

#### 7.4.9 场景 9：自动退款未认领（auto_refund.unclaimed）

```json
{
  "event": "auto_refund.unclaimed",
  "event_id": "evt_yyy",
  "order_id": "20290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "ORDER-10001",
  "amount": "100",
  "currency": "USD",
  "status": "partial_paid",
  "amount_confirmed": "50",
  "amount_confirming": "0",
  "created_at": 1763512349,
  "updated_at": 1766100000,
  "payments": [
    {
      "id": "pay_8b9c0d1e2f3a",
      "method": "Crypto Transfer",
      "status": "paid",
      "currency": "USD",
      "amount": "50",
      "paid_currency": "USDT",
      "paid_amount": "50",
      "network": "TRON",
      "tx_hash": "4e82a9b1...",
      "created_at": 1763512400,
      "completed_at": 1763512500
    }
  ],
  "auto_refund": {
    "id": "ref_7f8e9d0c1b2a",
    "original_payment_ids": ["pay_8b9c0d1e2f3a"],
    "reason": "underpayment",
    "status": "unclaimed",
    "currency": "USDT",
    "refund_amount": "50",
    "gas_fee": "1",
    "received_amount": "49",
    "network": "TRON",
    "refund_address": null,
    "tx_hash": null,
    "claim_period_days": 30,
    "claim_deadline": 1766100000
  }
}
```

#### 7.4.10 场景 10：订阅激活（subscription.update）

首次支付完成后，订阅从 pending 状态变为 active。

```json
{
  "event": "subscription.update",
  "subscription_id": "sub-9f3c1f2e",
  "merchant_sub_id": "msub_001",
  "plan_name": "Monthly Plan",
  "trigger_method": "invoice",
  "status": "active",
  "currency": "USD",
  "amount": "9.99",
  "interval_unit": "MONTH",
  "interval_count": 1,
  "payer_email": "user@example.com",
  "current_period_start": 1740000000,
  "current_period_end": 1742678400,
  "next_invoice_at": 1742592000,
  "created_at": 1740000000,
  "updated_at": 1740000100
}
```

#### 7.4.11 场景 11：订阅续费（subscription.update）

续费支付完成后，计费周期更新至下一个周期。

```json
{
  "event": "subscription.update",
  "subscription_id": "sub-9f3c1f2e",
  "merchant_sub_id": "msub_001",
  "plan_name": "Monthly Plan",
  "trigger_method": "invoice",
  "status": "active",
  "currency": "USD",
  "amount": "9.99",
  "interval_unit": "MONTH",
  "interval_count": 1,
  "payer_email": "user@example.com",
  "current_period_start": 1742678400,
  "current_period_end": 1745356800,
  "next_invoice_at": 1745270400,
  "created_at": 1740000000,
  "updated_at": 1742678500
}
```

#### 7.4.12 场景 12：订阅取消（subscription.cancel）

订阅被商户 API 取消、用户退订、或系统因未支付自动取消。

```json
{
  "event": "subscription.cancel",
  "subscription_id": "sub-9f3c1f2e",
  "merchant_sub_id": "msub_001",
  "plan_name": "Monthly Plan",
  "trigger_method": "invoice",
  "status": "canceled",
  "currency": "USD",
  "amount": "9.99",
  "interval_unit": "MONTH",
  "interval_count": 1,
  "payer_email": "user@example.com",
  "current_period_start": 1742678400,
  "current_period_end": 1745356800,
  "cancel_reason": "by_merchant_api",
  "canceled_at": 1743000000,
  "created_at": 1740000000,
  "updated_at": 1743000000
}
```

### 7.5 接收 Webhook 与安全校验

Infini 会向你配置的 Webhook URL 发起 POST 请求，建议接收端遵循以下原则：

1. 校验所有必需 Header 是否存在：


- X-Webhook-Signature
- X-Webhook-Timestamp
- X-Webhook-Event-Id


1. 验证签名合法性（见下一小节）。
2. 基于 X-Webhook-Event-Id 实现幂等处理（如仅处理一次）。
3. 业务逻辑应 **异步处理**，快速返回 HTTP 200，避免超时。


### 7.6 Webhook 签名校验（Signature Verification）

验签步骤：

1. 从 Header 中读取：


- X-Webhook-Signature（签名）
- X-Webhook-Timestamp（时间戳）
- X-Webhook-Event-Id（事件 ID）


1. 获取原始请求体字符串 payload。
2. 组装签名内容字符串：


```
{timestamp}.{event_id}.{payload}
```

1. 使用你的 WEBHOOK_SECRET 做 HMAC-SHA256 计算：


```python
signed_content = f"{timestamp}.{event_id}.{payload}"
expected_sig = hmac.new(
WEBHOOK_SECRET.encode(),
signed_content.encode(),
hashlib.sha256
).hexdigest()
```

1. 对比 expected_sig 与 X-Webhook-Signature 是否一致。


#### 7.6.1 Python 验签示例

```python
@app.route('/webhook', methods=['POST'])
def webhook_verification():
    signature = request.headers.get('X-Webhook-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')
    event_id = request.headers.get('X-Webhook-Event-Id')

    if not all([signature, timestamp, event_id]):
        return jsonify({"error": "Missing required headers"}), 400

    payload = request.get_data(as_text=True)
    signed_content = f"{timestamp}.{event_id}.{payload}"
    expected_sig = hmac.new(
        WEBHOOK_SECRET.encode(),
        signed_content.encode(),
        hashlib.sha256
    ).hexdigest()

    if expected_sig != signature:
        return jsonify({"error": "Invalid signature"}), 401

    # Process valid webhook
    return jsonify({"status": "ok"})
```

### 7.7 Webhook 重试策略

若商户端未返回 HTTP 200，Infini 会对该事件进行重试。

- **最多重试：8 次**
- **前 3 次重试间隔：30 秒**
- **第 4～8 次采用递增退避策略**，示例：


| 尝试次数 | 说明 | 间隔时间（示例） |
|  --- | --- | --- |
| 第 1 次 | 首次发送 | 立即 |
| 第 2 次 | 第 1 次失败 | 30 秒 |
| 第 3 次 | 第 2 次失败 | 30 秒 |
| 第 4 次 | 第 3 次失败 | 30 秒 |
| 第 5 次 | 第 4 次失败 | 60 秒 |
| 第 6 次 | 第 5 次失败 | 120 秒 |
| 第 7 次 | 第 6 次失败 | 240 秒 |
| 第 8 次 | 第 7 次失败 | 480 秒 |


> 若最终多次重试仍失败，该事件将被标记为投递失败，建议商户通过日志与对账工具进行排查。