# Webhooks (/zh/docs/cartao/webhooks)

<QuickLinks>
  <QuickLink href="/docs/cartao/endpoints/charges/post_charges" title="创建收款" method="POST" path="/charges" />

  <QuickLink href="/docs/cartao/endpoints/charges/get_charges__chargeId_" title="查询收款" method="GET" path="/charges/{chargeId}" />

  <QuickLink href="/docs/cartao/recurrence" title="循环扣款" />

  <QuickLink href="/docs/cartao/transaction-status" title="交易状态" />
</QuickLinks>

您的系统无需反复轮询"付款了吗?",有事件发生时 PayZu 会**主动调用您**:收款状态变化、反欺诈状态更新、拒付(chargeback)或新的循环扣款周期。

## 如何配置 [#如何配置]

在创建收款时([`POST /charges`](/docs/cartao/endpoints/charges/post_charges))提供 `postbackUrl`。每当有事件发生,PayZu 都会向该 URL 发送一个 JSON 格式的 `POST` 请求。

## 事件 [#事件]

| 事件类型               | 说明                                         |
| ------------------ | ------------------------------------------ |
| `charge.update`    | 支付状态变化                                     |
| `antifraud.update` | 反欺诈分析完成,结果体现在收款的 `status` 和 `reasonCode` 中 |
| `chargeback`       | 拒付通知                                       |
| `recurrence.cycle` | 新的[循环扣款](/docs/cartao/recurrence)周期已扣款     |

## Payload 结构 [#payload-结构]

| 参数      | 说明             | 类型                                                                   |
| ------- | -------------- | -------------------------------------------------------------------- |
| `event` | 触发 webhook 的事件 | 参见[事件](#事件)表                                                         |
| `data`  | 收款的最新数据        | 与[查询收款](/docs/cartao/endpoints/charges/get_charges__chargeId_)返回的值相同 |

```json
{
  "event": "charge.update",
  "data": {}
}
```

`data` 对象的格式与[查询收款](/docs/cartao/endpoints/charges/get_charges__chargeId_)的响应完全一致。

## 请求 Header [#请求-header]

每个 `POST` 请求都带有以下 header:

| Header                | 说明                                       |
| --------------------- | ---------------------------------------- |
| `Content-Type`        | 始终为 `application/json`                   |
| `X-Webhook-Signature` | Payload 的 HMAC SHA-256 签名,十六进制格式(64 个字符) |
| `X-Webhook-Timestamp` | 发送时刻,自 Unix 纪元起的毫秒数                      |
| `X-Webhook-Nonce`     | 请求的唯一标识符(32 个十六进制字符)                     |

## 重试 [#重试]

事件发生后会立即进行首次投递。只有当您的 URL 在 **5 秒**内返回 HTTP `2xx` 状态码,投递才视为成功:任何其他状态码或超时的响应均视为失败。

投递失败后,webhook 最多进行 **5 次重试**。每次失败后,距下一次尝试的间隔逐步增大:各次重试分别在 1 分钟、10 分钟、1 小时、6 小时和 24 小时后进行。此后不再重试。

<Callout type="info">
  请尽快响应 webhook(返回一个简单的 `200` 即可),并异步处理 payload,以免超出 5 秒的限制。由于超时可能导致已处理过的事件被重新投递，消费必须是幂等的：请使用收款的 `id` 加上状态变化作为去重键。不要用 `X-Webhook-Nonce` 去重，它标识的是 HTTP 请求，每次重新投递都会变化。
</Callout>

<Mermaid
  chart="`
flowchart TD
  A[&#x22;收款发生事件&#x22;]
  A --> B[&#x22;PayZu 发送 POST postbackUrl&#x22;]
  B --> C{&#x22;5 秒内返回 2xx?&#x22;}
  C -->|是| D[&#x22;投递确认&#x22;]
  C -->|否| E[&#x22;等待:1 分钟、10 分钟、1 小时、6 小时、24 小时&#x22;]
  E --> F{&#x22;重试次数少于 5?&#x22;}
  F -->|是| B
  F -->|否| G[&#x22;停止重试&#x22;]
`"
/>

## HMAC 验证 [#hmac-验证]

每个 webhook 都使用您的 **webhook secret** 签名,该密钥由 PayZu 随您的 [API 凭据](/docs/cartao/authentication)一同提供。您的 API 应在处理 payload 之前先验证签名:

<Steps>
  <Step>
    提取 `x-webhook-timestamp`、`x-webhook-nonce` 和 `x-webhook-signature` 三个 header。
  </Step>

  <Step>
    将 timestamp、nonce 和 payload 的值用 `.` 连接,得到验证基础字符串:`timestamp.nonce.payload`。
  </Step>

  <Step>
    使用您的 webhook secret,对该字符串以 SHA-256 算法生成 HMAC 签名。
  </Step>

  <Step>
    将生成的签名与 `x-webhook-signature` header 的值比较。不一致则拒绝该 webhook。
  </Step>
</Steps>

Node.js 示例,使用 `crypto.timingSafeEqual` 以恒定时间比较签名:

```js
const crypto = require("node:crypto");

function verifyWebhookSignature(request, webhookSecret) {
  const timestamp = request.headers["x-webhook-timestamp"];
  const nonce = request.headers["x-webhook-nonce"];
  const signature = request.headers["x-webhook-signature"];

  if (typeof signature !== "string" || !/^[0-9a-f]{64}$/i.test(signature)) {
    return false;
  }

  const baseString = `${timestamp}.${nonce}.${request.rawBody}`;
  const expectedSignature = crypto
    .createHmac("sha256", webhookSecret)
    .update(baseString)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature, "hex"),
    Buffer.from(signature, "hex"),
  );
}
```

<Callout type="info">
  HMAC 必须基于请求的原始报文体(raw body)计算,即收到的原样内容,在任何 JSON 解析之前。
</Callout>

## Nonce 验证(可选) [#nonce-验证可选]

`x-webhook-nonce` header 的值是每个请求唯一且临时的标识符。提取后,检查该 nonce 是否已被记录过:

* 若该值已被使用过,则拒绝请求,以防范重放攻击(replay attacks)。
* 若 nonce 是新的,则将其记录为已使用,确保后续调用无法重复使用。

## Timestamp 验证(可选) [#timestamp-验证可选]

`x-webhook-timestamp` header 的值是发送时刻自 Unix 纪元起的**毫秒**数。将其与当前时间比较:若差值超过 **5 分钟**,则拒绝请求。此项校验可丢弃已过期的 webhook,避免处理陈旧或潜在恶意的消息。