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

# Webhook

> 注册 Webhook 端点，接收来自 VelaFi 的订单状态变更、资金事件和全球支付更新的即时 HTTP 回调。

Webhook 让 VelaFi 能够在重要事件发生时立即向您的服务器推送实时通知——例如订单状态变更、资金到账，或全球支付在其生命周期中流转。您无需反复轮询 API，只需注册一次 HTTPS 端点，每当订阅的事件触发时，VelaFi 就会向该端点发送带有 JSON 负载的 HTTP POST 请求。本指南介绍如何注册端点、验证请求签名以及确认接收。

> [!WARNING]
>
> Webhook **不会在沙盒环境中触发**（`https://api-test.velafi.com`）。请在本地模拟负载来测试 Webhook 处理逻辑，然后在生产环境中使用小额交易进行端到端验证。

## 可用事件类型

为您希望接收的每种事件类型分别注册一个 Webhook。如果某事件类型已存在有效的 Webhook，则创建新 Webhook 的请求将被拒绝；Webhook 会接收您账户下所有商户的对应事件。VelaFi 支持以下事件类型：

> [card] ORDER\_WEBHOOK
>
> 通过 API 创建的法币兑加密货币（入金）和加密货币兑法币（出金）订单发生状态变更时触发。

> [card] GLOBAL\_PAYMENT\_WEBHOOK
>
> 通过 API 创建的法币兑法币全球支付订单发生状态变更时触发。

> [card] FUNDING\_WEBHOOK
>
> 商户法币（待处理资金 / Pending Fund）账户上的资金记录（法币充值、法币提现、购买加密货币或内部划转）完成、取消或退款时触发。加密货币钱包提现则由 `CRYPTO_WITHDRAW_WEBHOOK` 推送。

> [card] ON\_RAMP\_WITHDRAW\_WEBHOOK
>
> VelaFi 已将已完成的 API 入金订单中的加密货币提现至订单目标钱包地址时触发。

> [card] STABLE\_COIN\_PAYMENT\_WEBHOOK
>
> 稳定币支付订单确认时触发。

> [card] [CRYPTO\_WITHDRAW\_WEBHOOK](https://docs.velafi.com/cn/api-reference/webhooks/crypto-withdraw-message-push.md)
>
> 商户钱包的加密货币提现成功、失败或被取消时触发。

## 注册 Webhook 端点

### 发送注册请求

调用 `POST /v2/webhook`，传入事件类型以及您希望 VelaFi 调用的 HTTPS URL。

**端点：** `POST /v2/webhook`

#### 请求参数

**eventType** `string` *required*

要订阅的事件类型：上述事件类型之一（`ORDER_WEBHOOK`、`GLOBAL_PAYMENT_WEBHOOK`、`FUNDING_WEBHOOK`、`ON_RAMP_WITHDRAW_WEBHOOK`、`STABLE_COIN_PAYMENT_WEBHOOK` 或 `CRYPTO_WITHDRAW_WEBHOOK`）。

**url** `string` *required*

回调端点的完整 URL（例如 `https://yourapp.com/webhooks/velafi`）。必须以 `http` 开头。请使用 `https://` URL。该端点必须可公开访问，并能够处理 HTTP POST 请求。

#### 请求示例

```bash theme={null}
curl -X POST https://api.velafi.com/v2/webhook \
  -H "X-BH-TOKEN: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "ORDER_WEBHOOK",
    "url": "https://yourapp.com/webhooks/velafi"
  }'
```

### 保存响应中的 publicKey

响应会返回 `webhookId`，以及非常重要的 `publicKey`，您需要用它来验证每个传入 Webhook 请求的签名。请妥善保存此密钥。如果丢失，可通过 `GET /v2/webhooks` 再次获取。

#### 响应示例

```json theme={null}
{
  "code": 200,
  "msg": "SUCCESS",
  "data": {
    "webhookId": "ebab8a9663704e7cbbebe672c7c67912",
    "eventType": "ORDER_WEBHOOK",
    "url": "https://yourapp.com/webhooks/velafi",
    "status": 10,
    "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp/6GRTIrl+kQb9u27cGz..."
  }
}
```

#### 响应字段

**webhookId** `string`

此 Webhook 注册的唯一标识符。

**eventType** `string`

您订阅的事件类型，用于确认您的请求。

**url** `string`

事件触发时 VelaFi 发送 POST 请求的回调 URL。

**status** `number`

Webhook 配置状态：`10` = 已启用，`20` = 已禁用，`30` = 已删除。

**publicKey** `string`

RSA 公钥（Base64 编码，X.509 格式），用于验证传入 Webhook 请求中的 `signature` 请求头。请妥善保存。

## 验证 Webhook 签名

VelaFi 发送到您端点的每个请求都包含 `signature` 请求头。该值是对原始请求体的 RSA-SHA256 签名，以十六进制字符串编码。在处理负载之前，请务必验证此签名，以确保请求确实来自 VelaFi。

验证步骤如下：

1. 从请求头中提取 `signature` 值。
2. 对 `signature` 值进行十六进制解码。
3. 使用 Webhook 注册响应中的 `publicKey`，将其作为针对原始请求体的 RSA-SHA256（`SHA256withRSA`，PKCS#1 v1.5）签名进行验证。
4. 如果验证成功，处理负载并返回 `{"callbackStatus": "SUCCESS"}`。

> [tab] Java
>
> ```java theme={null}
> import java.security.*;
> import java.security.spec.X509EncodedKeySpec;
> import java.util.Base64;
> import javax.servlet.http.HttpServletRequest;
>
> public class CallbackController {
>
>     // Replace with the publicKey returned at webhook registration
>     private String publicKey = "YOUR_PUBLIC_KEY";
>
>     public String callbackNotice(HttpServletRequest request, @RequestBody String param) {
>         // 1. Extract the signature header
>         String signature = request.getHeader("signature");
>
>         // 2. Verify using RSA-SHA256 (signature is hex-encoded)
>         boolean verified = verifyRSASHA256(param, signature, publicKey);
>         System.out.println(String.format("verified:%s", verified));
>
>         return String.format(
>             "{\"callbackStatus\":\"%s\"}",
>             verified ? "SUCCESS" : "FAIL"
>         );
>     }
>
>     public static boolean verifyRSASHA256(String data, String signature, String publicKeyStr) {
>         try {
>             // Decode the Base64-encoded public key
>             byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyStr);
>             X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
>
>             // Reconstruct the RSA public key
>             KeyFactory keyFactory = KeyFactory.getInstance("RSA");
>             PublicKey publicKey = keyFactory.generatePublic(keySpec);
>
>             // Initialize the Signature verifier
>             Signature sig = Signature.getInstance("SHA256withRSA");
>             sig.initVerify(publicKey);
>
>             // Feed the payload bytes
>             sig.update(data.getBytes("UTF-8"));
>
>             // Decode the hex-encoded signature and verify
>             byte[] signatureBytes = decodeHex(signature);
>             return sig.verify(signatureBytes);
>         } catch (Exception e) {
>             e.printStackTrace();
>             return false;
>         }
>     }
>
>     public static byte[] decodeHex(String value) {
>         int len = value.length();
>         byte[] data = new byte[len / 2];
>         for (int i = 0; i < len; i += 2) {
>             data[i / 2] = (byte) (
>                 (Character.digit(value.charAt(i), 16) << 4)
>                 + Character.digit(value.charAt(i + 1), 16)
>             );
>         }
>         return data;
>     }
> }
> ```

> [tab] Python
>
> ```python theme={null}
> import json
> from flask import request
>
> # Replace with the publicKey returned at webhook registration
> PUBLIC_KEY = "YOUR_PUBLIC_KEY"
>
> def callback_notice():
>     # 1. Read the raw body and signature header
>     param = request.get_data(as_text=True)
>     signature = request.headers.get("signature")
>
>     # 2. Verify RSA-SHA256 (signature is hex-encoded)
>     verified = sha256_rsa_verify(param, signature, PUBLIC_KEY)
>     print(f"verified: {verified}")
>
>     # 3. Acknowledge receipt
>     return json.dumps({
>         "callbackStatus": "SUCCESS" if verified else "FAIL"
>     })
>
> def sha256_rsa_verify(data: str, signature: str, public_key_b64: str) -> bool:
>     """
>     Verify an RSA-SHA256 signature.
>
>     :param data:           The raw request body string that was signed.
>     :param signature:      Hex-encoded RSA-SHA256 signature from the header.
>     :param public_key_b64: Base64-encoded (X.509) RSA public key.
>     :return:               True if the signature is valid, False otherwise.
>     """
>     from cryptography.hazmat.primitives import hashes, serialization
>     from cryptography.hazmat.primitives.asymmetric import padding
>     import base64
>
>     try:
>         pub_key_bytes = base64.b64decode(public_key_b64)
>         public_key = serialization.load_der_public_key(pub_key_bytes)
>         sig_bytes = bytes.fromhex(signature)
>         public_key.verify(
>             sig_bytes,
>             data.encode("utf-8"),
>             padding.PKCS1v15(),
>             hashes.SHA256()
>         )
>         return True
>     except Exception:
>         return False
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> const crypto = require("crypto");
>
> // Replace with the publicKey returned at webhook registration
> const PUBLIC_KEY = "YOUR_PUBLIC_KEY";
>
> async function callbackNotice(req, res) {
>     // 1. Read the raw body and signature header
>     const param = req.body; // raw string — ensure body-parser is set to 'text'
>     const signature = req.headers["signature"];
>
>     // 2. Verify RSA-SHA256 (signature is hex-encoded)
>     const verified = verifyRSASHA256(param, signature, PUBLIC_KEY);
>     console.log(`verified: ${verified}`);
>
>     // 3. Acknowledge receipt
>     res.json({ callbackStatus: verified ? "SUCCESS" : "FAIL" });
> }
>
> function verifyRSASHA256(data, hexSignature, publicKeyB64) {
>     try {
>         // Reconstruct the PEM-formatted public key from Base64
>         const pem = `-----BEGIN PUBLIC KEY-----\n${publicKeyB64}\n-----END PUBLIC KEY-----`;
>         const verify = crypto.createVerify("RSA-SHA256");
>         verify.update(data, "utf8");
>         // Convert hex signature to Buffer
>         const sigBuffer = Buffer.from(hexSignature, "hex");
>         return verify.verify(pem, sigBuffer);
>     } catch (err) {
>         console.error(err);
>         return false;
>     }
> }
> ```

## Webhook 负载结构

每种事件类型都会向您的端点发送不同的 JSON 请求体。

> [accordion] ORDER\_WEBHOOK — 入金和出金订单
>
> 通过 API 创建的法币兑加密货币和加密货币兑法币订单发生状态变更时，VelaFi 会发送此负载。以下为简化示例；完整字段请参阅[订单事件](https://docs.velafi.com/cn/api-reference/webhooks/fiat-crypto-order-message-push.md)。
>
> ```json theme={null}
> {
>   "webhookId": "ebab8a9663704e7cbbebe672c7c67912",
>   "eventType": "ORDER_WEBHOOK",
>   "orderId": "473130522693193728",
>   "clientId": "W658784738723",
>   "userId": "455472",
>   "merchantId": "4",
>   "orderType": "SELL",
>   "fiat": "MXN",
>   "fiatAmount": "99",
>   "fiatFee": "0",
>   "crypto": "USDT",
>   "cryptoAmount": "5",
>   "orderPrice": "19.8158",
>   "orderStatus": "60",
>   "createTime": "1735279907000",
>   "updateTime": "1735280582000"
> }
> ```
>
> 入金订单的 `orderType` 为 `BUY`，出金订单为 `SELL`。仅部分状态会被推送，被拒绝或超时的订单以 `70` 推送。推送的状态请参阅[订单事件](https://docs.velafi.com/cn/api-reference/webhooks/fiat-crypto-order-message-push.md)。

> [accordion] GLOBAL\_PAYMENT\_WEBHOOK — 法币兑法币订单
>
> 通过 API 创建的法币兑法币全球支付订单发生状态变更时，VelaFi 会发送此负载。
>
> ```json theme={null}
> {
>   "webhookId": "ebab8a9663704e7cbbebe672c7c67912",
>   "eventType": "GLOBAL_PAYMENT_WEBHOOK",
>   "orderId": "473130522693193728",
>   "clientId": "W658784738723",
>   "userId": "455472",
>   "onRampCountry": "Mexico",
>   "onRampMerchantId": "3",
>   "onRampFiat": "MXN",
>   "onRampFiatAmount": "100",
>   "onRampFiatFee": "0",
>   "offRampCountry": "Hong Kong",
>   "offRampMerchantId": "4",
>   "offRampFiat": "USD",
>   "offRampFiatAmount": "18",
>   "offRampFiatFee": "2",
>   "orderPrice": "20",
>   "orderStatus": "41",
>   "createTime": "1735279907000",
>   "updateTime": "1735280582000"
> }
> ```
>
> `orderStatus` 使用与[全球支付](https://docs.velafi.com/cn/docs/guides/global-payments.md)订单详情端点相同的状态码，但仅推送[全球支付事件](https://docs.velafi.com/cn/api-reference/webhooks/fiat-fiat-order-message-push.md)中列出的状态。

> [accordion] FUNDING\_WEBHOOK — 商户资金事件
>
> 当您商户法币（待处理资金 / Pending Fund）账户上的法币充值、法币提现、购买加密货币或内部划转完成、取消或退款时，VelaFi 会发送此负载。加密货币钱包提现则由 `CRYPTO_WITHDRAW_WEBHOOK` 推送。
>
> ```json theme={null}
> {
>   "webhookId": "ebab8a9663704e7cbbebe672c7c67912",
>   "eventType": "FUNDING_WEBHOOK",
>   "txId": "1123123123",
>   "clientId": "client123456",
>   "userId": "455472",
>   "merchantId": "3",
>   "merchantName": "Example Merchant",
>   "fiat": "ARS",
>   "totalAmount": "22",
>   "amount": "20",
>   "fee": "2",
>   "refundedAmount": "0",
>   "paymentId": "63",
>   "userPaymentId": "91",
>   "type": "DEPOSIT",
>   "status": "2",
>   "completedTime": "1737452344000",
>   "createTime": "1737452292000",
>   "updateTime": "1737452344000",
>   "cardNo": "0000000001",
>   "taxId": "20000000001",
>   "payerName": "Jane Doe",
>   "payerAccount": "0000000000000000000005",
>   "depositAlias": "alias",
>   "bankWebhookInfo": "{}",
>   "failCode": "",
>   "failReason": ""
> }
> ```
>
> `type` 字段为 `DEPOSIT`、`WITHDRAW`、`BUY_CRYPTO`、`TRANSFER_IN` 或 `TRANSFER_OUT` 之一。`status` 字段为 `2`（已完成）、`3`（已取消）或 `4`（已退款）。处理中的记录不会被推送。

## 确认接收

您的端点必须返回 HTTP `2xx` 响应（例如 `200`），并附带以下 JSON 请求体，以确认通知已成功接收和处理。如果 VelaFi 未收到 `SUCCESS` 确认（例如您的端点未在 6 秒内响应），将重试推送，总共尝试三次。有关重试时间表、重复处理和顺序，请参阅[推送与重试](https://docs.velafi.com/cn/api-reference/webhooks/webhook-setup.md#delivery-and-retries)。

```json theme={null}
{
  "callbackStatus": "SUCCESS"
}
```

> [!NOTE]
>
> 在验证签名并持久化事件后，请立即返回 `{"callbackStatus": "SUCCESS"}`。任何下游处理都应异步执行，以避免因超时触发不必要的重试。


## Related topics

- [Webhook 设置](https://docs.velafi.com/cn/api-reference/webhooks/webhook-setup.md)
- [删除 Webhook](https://docs.velafi.com/cn/api-reference/webhooks/delete-a-webhook.md)
- [更新 Webhook](https://docs.velafi.com/cn/api-reference/webhooks/update-a-webhook.md)
- [创建 Webhook](https://docs.velafi.com/cn/api-reference/webhooks/create-webhook-endpoints.md)
- [获取 Webhook 列表](https://docs.velafi.com/cn/api-reference/webhooks/get-webhook-endpoints.md)


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