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

# Webhook 概览

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

## 为什么使用 Webhook

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

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

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

## 前置条件

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

## Webhook 请求头

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

| HEADER | DESCRIPTION |
| :- | :- |
| x-wk-timestamp | 13 位 Unix 毫秒时间戳，例如 1711077773123。计算签名时，请将该 header 值作为字符串处理。 |
| x-wk-signature | 响应体的 HMAC512 十六进制摘要。当 Webhook 配置了 secret 时，UQPAY 会带上该 header。HMAC512 摘要由 sha512 散列函数生成，并以 secret 作为 HMAC 的 key。 |

## 示例事件 payload

```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": ""
  }
}
```

## IP 白名单

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

### 沙盒环境

* `52.76.137.90`、`52.221.8.28`

### 生产环境

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

## 签名验证

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 做格式化 — 请在任何转换发生之前完成签名校验。

### 代码示例

> [tab] 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] 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] 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] 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] TypeScript
>
> ```typescript theme={null}
> // Koa；请在 JSON 解析前保存原始请求体
> const crypto = require('crypto')
>
> const secret = '<YOUR_WEBHOOK_SECRET>'
>
> async webhookController(ctx, next) {
>   // webhook is received
>
>   const { headers, rawBody } = ctx.request
>   const timestamp = headers['x-wk-timestamp']
>
>   const payload = rawBody + 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
>   }
> }
> ```

## 重试策略

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

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

## 最佳实践

### 立即确认事件

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

### 处理重复事件

> [!WARNING]
>
> **重要 — 处理重复事件**
>
> Webhook 端点偶尔会收到同一事件的多次投递。建议通过**让事件处理逻辑保持幂等**来避免重复处理。
>
> 去重时请使用事件 payload 中的 `event_id` 字段。所有重试尝试（例如未向 UQPAY 返回成功的 HTTP 200 响应）都会复用同一个 `event_id`，你可以据此安全地忽略重复事件。

### 事件顺序

UQPAY 不保证事件按其生成顺序投递。你的端点不应假设事件按生成顺序到达，需自行处理乱序场景。当事件类型提供资源专属的版本号或序列字段时，请使用该字段排序。`x-wk-timestamp` 只作为辅助投递元数据。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.