> ## 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 设置

> 配置您的服务器以接收 VelaFi Webhook 事件，并使用 Java、Python 和 JavaScript 示例代码验证 RSA-SHA256 负载签名。

VelaFi 以 HTTP POST 请求的形式向您注册的 URL 发送事件通知，负载为 JSON 格式。为了保护您的端点免受伪造请求的攻击，VelaFi 使用 RSA-SHA256 对每个 Webhook 负载进行签名，并将十六进制编码的签名放在 `signature` 请求头中。您的服务器必须在处理事件之前验证此签名。

## 签名验证的工作原理

**1. 提取签名请求头**

从传入的 HTTP 请求中读取 `signature` 请求头。该值是原始请求体的 RSA-SHA256 签名，编码为十六进制字符串。

**2. 加载您的公钥**

获取创建 Webhook 端点时返回的 `publicKey`。[列出 Webhook](https://docs.velafi.com/cn/api-reference/webhooks/get-webhook-endpoints.md) 的响应中也会为每个 Webhook 包含该公钥。这是一个 X.509 格式、Base64 编码的 RSA 公钥。

**3. 计算并验证签名**

使用 RSA-SHA256，基于原始请求体字节和公钥验证签名。验证前请先将十六进制签名解码为字节。

**4. 响应 VelaFi**

如果验证通过，返回 `{"callbackStatus": "SUCCESS"}`；否则返回 `{"callbackStatus": "FAIL"}`。VelaFi 根据此响应决定是否重试推送。

> [!WARNING]
>
> 在处理 Webhook 负载之前，务必先验证签名。处理未经验证的事件可能使您的系统遭受重放攻击和欺诈性的状态变更。

## 签名验证代码

> [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 {
>
>     // Place your public key according to the environment
>     private String publicKey = "YOUR_PUBLIC_KEY";
>
>     public String callbackNotice(HttpServletRequest request, String param) {
>         // Step 1: Extract the signature header
>         String signature = request.getHeader("signature");
>
>         // Step 2: Verify using RSA-SHA256 (hex-encoded signature)
>         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 {
>             // 1. Decode the Base64-encoded public key
>             byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyStr);
>             X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
>
>             // 2. Generate RSA public key
>             KeyFactory keyFactory = KeyFactory.getInstance("RSA");
>             PublicKey publicKey = keyFactory.generatePublic(keySpec);
>
>             // 3. Initialize Signature object
>             Signature sig = Signature.getInstance("SHA256withRSA");
>             sig.initVerify(publicKey);
>
>             // 4. Update the data to be verified
>             sig.update(data.getBytes("UTF-8"));
>
>             // 5. Decode the hexadecimal signature
>             byte[] signatureBytes = decodeHex(signature);
>
>             // 6. Verify
>             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
> from Crypto.PublicKey import RSA
> from Crypto.Signature import pkcs1_15
> from Crypto.Hash import SHA256
> import base64
> import binascii
>
> # Place your public key according to the environment
> public_key_b64 = "YOUR_PUBLIC_KEY"
>
> def callback_notice():
>     # Step 1: Extract the raw body and signature header
>     param = request.get_data(as_text=True)
>     signature_hex = request.headers.get('signature')
>
>     # Step 2: Verify RSA-SHA256 signature (hex-encoded)
>     verified = sha256_rsa_verify(param, signature_hex, public_key_b64)
>     print(f"verified:{verified}")
>
>     return json.dumps({"callbackStatus": "SUCCESS" if verified else "FAIL"})
>
> def sha256_rsa_verify(data: str, signature_hex: str, public_key_b64: str) -> bool:
>     try:
>         # Decode the Base64 public key and import it
>         pub_key_bytes = base64.b64decode(public_key_b64)
>         public_key = RSA.import_key(pub_key_bytes)
>
>         # Hash the message
>         h = SHA256.new(data.encode("utf-8"))
>
>         # Decode the hex signature to bytes
>         sig_bytes = binascii.unhexlify(signature_hex)
>
>         # Verify
>         pkcs1_15.new(public_key).verify(h, sig_bytes)
>         return True
>     except (ValueError, TypeError):
>         return False
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> const crypto = require('crypto');
>
> // Place your public key according to the environment
> // The key should be in PEM format or you can derive it from the Base64 string
> const publicKeyBase64 = "YOUR_PUBLIC_KEY";
>
> async function callbackNotice(req, param) {
>     // Step 1: Extract the signature header
>     const signatureHex = req.headers['signature'];
>
>     // Step 2: Verify the RSA-SHA256 signature (hex-encoded)
>     const verified = await verifyRSASHA256(param, signatureHex, publicKeyBase64);
>     console.log(`verified: ${verified}`);
>
>     return JSON.stringify({
>         callbackStatus: verified ? "SUCCESS" : "FAIL"
>     });
> }
>
> async function verifyRSASHA256(data, signatureHex, publicKeyBase64) {
>     try {
>         // Convert Base64 public key to PEM format
>         const pem = `-----BEGIN PUBLIC KEY-----\n${publicKeyBase64}\n-----END PUBLIC KEY-----`;
>
>         // Convert hex signature to Buffer
>         const signatureBuffer = Buffer.from(signatureHex, 'hex');
>
>         // Create verifier
>         const verify = crypto.createVerify('RSA-SHA256');
>         verify.update(data, 'utf8');
>
>         return verify.verify(pem, signatureBuffer);
>     } catch (err) {
>         console.error('Verification error:', err);
>         return false;
>     }
> }
> ```

## Webhook 事件类型

VelaFi 支持以下事件类型。每种事件类型都需要单独注册一个 Webhook 端点。如果该事件类型已存在一个活跃的 Webhook，创建 Webhook 的请求将被拒绝。Webhook 归属于您的 VelaFi 账户，因此一个 Webhook 会接收账户下所有商户的对应事件：

> [card] ORDER\_WEBHOOK
>
> 当通过 API 创建的法币兑加密货币或加密货币兑法币订单状态变更时触发。

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

> [card] FUNDING\_WEBHOOK
>
> 当资金记录（存款、提现、加密货币购买或内部转账）完成、取消或退款时触发。

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

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

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

> [!TIP]
>
> 请安全保存创建 Webhook 时返回的 `publicKey`。每次验证调用都需要用到它。如果您丢失了它，请调用[列出 Webhook](https://docs.velafi.com/cn/api-reference/webhooks/get-webhook-endpoints.md)，该接口会返回每个 Webhook 的 `publicKey`。

## 推送与重试

每次推送都是一个带有 JSON 请求体和 `signature` 请求头的 HTTP POST 请求。除事件特定字段外，每个负载都包含以下字段：

| 字段 | 描述 |
| - | - |
| `webhookId` | 该事件所排队推送的 Webhook ID。 |
| `eventType` | 事件类型。 |
| `timestamp` | 本次推送尝试签名时的 Unix 时间戳（毫秒）。每次尝试都会生成新值。 |

负载中的值（包括 ID、金额和时间戳）均以 JSON 字符串形式发送。

* **确认接收。** 只有当您的端点返回 `2xx` HTTP 状态，且响应体为 `callbackStatus` 等于 `SUCCESS` 的 JSON 对象时，推送才算成功。任何其他状态或响应体都视为一次失败的尝试。VelaFi 的连接、写入和读取超时均为 6 秒，超时同样视为一次失败的尝试。
* **重试。** 推送失败后通常会自动重试，总共尝试三次。第二次尝试在第一次失败后约 5–10 分钟进行，第三次尝试在第二次失败后约 60–65 分钟进行。第三次尝试失败后，该事件不再重试。Open API 不提供请求重新推送的端点。
* **重复推送。** 同一事件可能会被推送多次。例如，如果您的端点已处理某次推送，但其响应未能及时送达，该事件将被重试。请以事件类型、记录 ID（`orderId`、`txId` 或 `withdrawId`）和状态作为键，使您的处理逻辑具备幂等性。
* **顺序与跳过的状态。** VelaFi 不保证推送按照状态发生的顺序到达。对于 `ORDER_WEBHOOK`、`GLOBAL_PAYMENT_WEBHOOK`、`FUNDING_WEBHOOK` 和 `ON_RAMP_WITHDRAW_WEBHOOK`，VelaFi 会在每次尝试前检查记录的当前状态。如果自事件排队以来状态已发生变化，该事件将被丢弃且不会推送，因此您可能不会收到每一个中间状态。请查询订单或资金记录以获取其最新状态。`CRYPTO_WITHDRAW_WEBHOOK` 只推送提现的最终状态。
* **仅限活跃的 Webhook。** 只有当该事件类型存在活跃的 Webhook（状态 `10`）时，事件才会被排队。在没有活跃 Webhook 期间发生的事件不会被排队，之后也不会推送。Webhook 被禁用或删除时，已排队的事件不会被取消：它们会被保留，如果在约 15 天内该事件类型再次存在活跃的 Webhook，这些事件将以其原始 `webhookId` 推送到该 Webhook 的 URL。`CRYPTO_WITHDRAW_WEBHOOK` 事件例外：它们只会推送到其排队时所属的 Webhook，因此在删除或停用旧 Webhook 后新建的 Webhook 不会收到这些事件。


## Related topics

- [创建 Webhook](https://docs.velafi.com/cn/api-reference/webhooks/create-webhook-endpoints.md)
- [订单事件](https://docs.velafi.com/cn/api-reference/webhooks/fiat-crypto-order-message-push.md)
- [全球支付事件](https://docs.velafi.com/cn/api-reference/webhooks/fiat-fiat-order-message-push.md)
- [入金提现事件](https://docs.velafi.com/cn/api-reference/webhooks/on-ramp-withdraw-message-push.md)
- [加密货币提现事件](https://docs.velafi.com/cn/api-reference/webhooks/crypto-withdraw-message-push.md)


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