> ## Documentation Index
> Fetch the complete documentation index at: https://developers.uqpay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook 概览

> 配置 Webhook 以接收 UQPAY 的实时事件通知，并了解签名验证与投递处理方式。

<h2 id="why-use-webhooks">为什么使用 Webhook</h2>

在构建 UQPAY 集成时，你可能希望应用能在 UQPAY 账户发生事件时实时接收这些事件，从而让后端系统据此执行相应动作。

启用 Webhook 事件需要先注册 Webhook 端点。注册完成后，UQPAY 会在你的账户发生事件时，将实时事件数据推送到应用的 Webhook 端点。UQPAY 通过 HTTPS 向你的应用发送 Webhook 事件，payload 为包含 Event 对象的 JSON。

Webhook 特别适合监听异步事件，例如客户的银行确认支付、客户发起拒付、定期付款成功，以及订阅付款的收取场景。

<h2 id="prerequisites">前置条件</h2>

在阅读本文之前，请先在控制台完成 [Webhook 端点设置](/zh/account-center/v1.6/guide/webhooks-setting)。

<h2 id="webhook-headers">Webhook 请求头</h2>

UQPAY 发送到你 Webhook 端点 URL 的 HTTP POST payload 会包含以下特殊 header：

| <div style={{"width": "120px"}}>HEADER</div> | DESCRIPTION                                                                                                     |
| :------------------------------------------- | :-------------------------------------------------------------------------------------------------------------- |
| x-wk-timestamp                               | Long 型时间戳，例如 1711077773。                                                                                        |
| x-wk-signature                               | 响应体的 HMAC512 十六进制摘要。当 Webhook 配置了 secret 时，UQPAY 会带上该 header。HMAC512 摘要由 sha512 散列函数生成，并以 secret 作为 HMAC 的 key。 |

<h2 id="example-event-payload">示例事件 payload</h2>

```json theme={null}
{
  "version": "V1.6.0",
  "event_name": "ISSUING",
  "event_type": "issuing.transaction.authorization",
  "event_id": "234fca01-1ace-4d34-baf5-29b10e9d11c0",
  "source_id": "1a53aebf-900c-4e25-9852-b98f4338d94c",
  "data": {
    "authorization_code": "W6MJU9",
    "billing_amount": "11.5",
    "billing_currency": "USD",
    "card_available_balance": "988.5",
    "card_id": "50418faa-57a8-4ce2-9157-621b00b13a3b",
    "card_number": "40963608****1764",
    "cardholder_id": "25ea804d-7fd5-43d5-8792-0fc0214cdb2f",
    "description": "",
    "fee_pass_through": "Y",
    "merchant_data": [
      {
        "category_code": "5734",
        "city": "",
        "country": "",
        "name": "Test Merchant"
      }
    ],
    "original_transaction_id": "",
    "posted_time": "2026-04-12T15:27:39.563+08:00",
    "short_transaction_id": "T260412-2AHV51KO5U68",
    "transaction_amount": "10",
    "transaction_currency": "USD",
    "transaction_fee": "1.5",
    "transaction_fee_currency": "USD",
    "transaction_id": "1a53aebf-900c-4e25-9852-b98f4338d94c",
    "transaction_status": "APPROVED",
    "transaction_time": "2026-04-12T15:27:39.48+08:00",
    "transaction_type": "AUTHORIZATION",
    "wallet_type": ""
  }
}
```

<h2 id="ip-whitelist">IP 白名单</h2>

UQPAY 从以下 IP 之一发起 Webhook 调用。要成功接收 Webhook，请将这些 IP 加入白名单：

<h3 id="sandbox-environment">沙盒环境</h3>

* `52.76.137.90`、`52.221.8.28`

<h3 id="production-environment">生产环境</h3>

* `18.143.59.64`、`54.179.248.205`、`13.250.234.88`、`18.136.58.213`、`56.10.39.6`

<h2 id="signature-verification">签名验证</h2>

UQPAY 从一组固定的 IP 地址发送 Webhook 事件。请仅信任来自这些 IP 的事件。

此外，通过验证 Webhook 签名来确认收到的事件确由 UQPAY 发送。UQPAY 会对发送给你端点的 Webhook 事件进行签名，把签名放在事件的 `x-wk-signature` header 中，你可据此确认事件来自 UQPAY 而非第三方。你可以使用 UQPAY 的官方库校验，也可以用自有方案手动校验。

签名验证步骤：

1. 获取你端点的签名密钥。
2. 从请求头中提取 `x-wk-timestamp` 与 `x-wk-signature`。
3. 拼接出 `value_to_digest` 字符串：把原始 JSON payload（请求体，字符串形式）与 `x-wk-timestamp`（字符串形式）首尾相连。
4. 以 SHA-512 散列函数计算 HMAC。使用端点的签名密钥作为 key，以 `value_to_digest` 字符串作为 message。
5. 将计算出的签名与 `x-wk-signature` header 做比较。若匹配，再比较当前时间戳与收到时间戳之间的差值是否在你的容差范围内。

<Warning>
  **常见问题：签名不匹配**

  构造预期签名时，请务必使用原始 JSON payload。

  许多库在解析 payload 时会对 JSON 做格式化 — 请在任何转换发生之前完成签名校验。
</Warning>

<h3 id="code-examples">代码示例</h3>

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    ...
    payload = "" //webhook payload
    secret = '<YOUR_WEBHOOK_SECRET>'
    timestamp := c.GetHeader("x-wk-timestamp")
    signature := c.GetHeader("x-wk-signature")

    valueToDigest := payload + timestamp
    signatureHex := calculateHMACSHA512([]byte(secret), []byte(valueToDigest))

    //compare signatureHex with signature
    ...
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    ...
    StringBuilder valueToDigest = new StringBuilder();

    String payload = ""; //webhook payload

    // Get the timestamp, signature from header
    String timestamp = request.getHeader("x-wk-timestamp");
    String signature = request.getHeader("x-wk-signature");

    valueToDigest.append(payload);
    valueToDigest.append(timestamp);

    // Get your secret
    String secret = getSecret();

    String signatureHex = HmacUtils.hmacSha512Hex(secret, valueToDigest.toString())
    ...
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    <?php
    ...
      $timestamp = $request->getHeaderLine('x-wk-timestamp');
      $payload = $request->getBody()->getContents();

      $secret = <your signatuer secret>;
      $signature = $request->getHeaderLine('x-wk-signature');

      if (hash_hmac('sha512', $payload.$timestamp, $secret) != $signature) {
      	return new Response(400, array(), 'failed to verify the signature');
      }

      // Do something with event
      return new Response(200, array(), $body);
    }
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    use ring::hmac;

    fn verify_signature(payload: &str, timestamp: &str, received_signature: &str, secret: &str) -> bool {
        let key = hmac::Key::new(hmac::HMAC_SHA512, secret.as_bytes());
        let value_to_digest = format!("{}{}", payload, timestamp);
        let signature = hmac::sign(&key, value_to_digest.as_bytes());
        let signature_hex = hex::encode(signature.as_ref());

        signature_hex == received_signature
    }

    fn main() {
        let payload = ""; // Your webhook payload here
        let secret = "<YOUR_WEBHOOK_SECRET>";
        let timestamp = "x-wk-timestamp header value here";
        let received_signature = "x-wk-signature header value here";

        if verify_signature(payload, timestamp, received_signature, secret) {
            println!("Signature verified!");
        } else {
            println!("Signature verification failed!");
        }
    }
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // express.js
    const crypto = require('crypto')

    const secret = '<YOUR_WEBHOOK_SECRET>'

    async webhookController(ctx, next) {
      // webhook is received

      const { headers, body } = ctx.request
      const { name, accountId } = body || {} // payload

      const timestamp = headers['x-wk-timestamp']

      const payload = `${body}${timestamp}`

      const signatureHex = crypto.createHmac('sha512', secret).update(payload).digest('hex')

      if (signatureHex === headers['x-wk-signature']) {
        // do business logic after signature is verified

        return next() // http response code = 200: ack the webhook
      } else {
        ctx.status = 500
        ctx.body = 'failed to verify webhook signature'
        return
      }
    }
    ```
  </Tab>
</Tabs>

<h2 id="retry-logic">重试策略</h2>

当你的 Webhook 端点不可用或响应超时，UQPAY 会按指数退避策略重发通知，直到收到成功响应或达到重试上限。

重试按指数退避排期：第 N 次重试发生在 `2^(attempts+3)` 秒之后，首次间隔 16 秒，后续依次翻倍，最大间隔上限为 900 秒（15 分钟）。最多重试 5 次。

<h2 id="best-practices">最佳实践</h2>

<h3 id="acknowledge-events-immediately">立即确认事件</h3>

如果你的 Webhook 脚本包含复杂逻辑，可能在 UQPAY 观察到完整执行之前就已超时。请把 Webhook 的应答代码（返回 200 状态码以确认事件已收到）与该事件的后续业务处理逻辑分开。

<h3 id="handle-duplicate-events">处理重复事件</h3>

<Warning>
  **重要 — 处理重复事件**

  Webhook 端点偶尔会收到同一事件的多次投递。建议通过**让事件处理逻辑保持幂等**来避免重复处理。

  去重时请使用事件 payload 中的 `event_id` 字段。所有重试尝试（例如未向 UQPAY 返回成功的 HTTP 200 响应）都会复用同一个 `event_id`，你可以据此安全地忽略重复事件。
</Warning>

<h3 id="event-ordering">事件顺序</h3>

UQPAY 不保证事件按其生成顺序投递。你的端点不应假设事件按生成顺序到达，需自行处理乱序场景。你也可以利用事件中的 `x-wk-timestamp` 进行排序。
