> ## 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.

# Webhooks

> Register webhook endpoints to receive instant HTTP callbacks for order status changes, funding events, and global payment updates from VelaFi.

Webhooks let VelaFi push real-time notifications to your server the moment something significant happens — an order changes status, funds arrive in your account, or a global payment moves through its lifecycle. Instead of polling the API repeatedly, you register an HTTPS endpoint once and VelaFi sends an HTTP POST to it with a JSON payload each time a subscribed event fires. This guide covers registering endpoints, verifying the request signature, and acknowledging delivery.

> [!WARNING]
>
> Webhooks are **not triggered in the sandbox environment** (`https://api-test.velafi.com`). Test your webhook handler logic by simulating payloads locally, then verify end-to-end in production with low-value transactions.

## Available event types

Register a separate webhook for each event type you want to receive. Creating a webhook is rejected while an active webhook already exists for that event type, and a webhook receives its event for every merchant under your account. VelaFi supports the following event types:

> [card] ORDER\_WEBHOOK
>
> Fires on status changes for fiat-to-crypto (on-ramp) and crypto-to-fiat (off-ramp) orders created through the API.

> [card] GLOBAL\_PAYMENT\_WEBHOOK
>
> Fires on status changes for fiat-to-fiat global payment orders created through the API.

> [card] FUNDING\_WEBHOOK
>
> Fires when a funding record (a fiat deposit, fiat withdrawal, crypto purchase, or internal transfer on the merchant's fiat (Pending Fund) account) is completed, canceled, or refunded. Crypto wallet withdrawals are covered by `CRYPTO_WITHDRAW_WEBHOOK` instead.

> [card] ON\_RAMP\_WITHDRAW\_WEBHOOK
>
> Fires when VelaFi has withdrawn the crypto from a completed API on-ramp order to the order's destination wallet address.

> [card] STABLE\_COIN\_PAYMENT\_WEBHOOK
>
> Fires when a stablecoin payment order is confirmed.

> [card] [CRYPTO\_WITHDRAW\_WEBHOOK](https://docs.velafi.com/api-reference/webhooks/crypto-withdraw-message-push.md)
>
> Fires when a crypto withdrawal from a merchant wallet succeeds, fails, or is cancelled.

## Register a webhook endpoint

### Send the registration request

Call `POST /v2/webhook` with the event type and the HTTPS URL you want VelaFi to call.

**Endpoint:** `POST /v2/webhook`

#### Request parameters

**eventType** `string` *required*

The event type to subscribe to: one of the event types listed above (`ORDER_WEBHOOK`, `GLOBAL_PAYMENT_WEBHOOK`, `FUNDING_WEBHOOK`, `ON_RAMP_WITHDRAW_WEBHOOK`, `STABLE_COIN_PAYMENT_WEBHOOK`, or `CRYPTO_WITHDRAW_WEBHOOK`).

**url** `string` *required*

The fully-qualified URL of your callback endpoint (e.g., `https://yourapp.com/webhooks/velafi`). It must start with `http`. Use an `https://` URL. The endpoint must be publicly reachable and able to handle HTTP POST requests.

#### Example request

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

### Save the publicKey from the response

The response returns a `webhookId` and — importantly — a `publicKey` that you use to verify the signature on every incoming webhook request. Store this key securely. If you lose it, `GET /v2/webhooks` returns it again.

#### Example response

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

#### Response fields

**webhookId** `string`

Unique identifier for this webhook registration.

**eventType** `string`

The event type you subscribed to, confirming your request.

**url** `string`

The callback URL VelaFi will POST to when events fire.

**status** `number`

Webhook configuration status: `10` = active, `20` = disabled, `30` = deleted.

**publicKey** `string`

RSA public key (Base64-encoded, X.509 format) used to verify the `signature` header on incoming webhook requests. Store this securely.

## Verify the webhook signature

Every request VelaFi sends to your endpoint includes a `signature` header. The value is an RSA-SHA256 signature of the raw request body, encoded as a hexadecimal string. Always verify this signature before processing the payload to ensure the request genuinely came from VelaFi.

The verification steps are:

1. Extract the `signature` value from the request header.
2. Hex-decode the `signature` value.
3. Verify it as an RSA-SHA256 (`SHA256withRSA`, PKCS#1 v1.5) signature over the exact raw request body, using the `publicKey` from the webhook registration response.
4. If verification succeeds, process the payload and return `{"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 payload schemas

Each event type delivers a distinct JSON body to your endpoint.

> [accordion] ORDER\_WEBHOOK — on-ramp and off-ramp orders
>
> VelaFi sends this payload on status changes for fiat-to-crypto and crypto-to-fiat orders created through the API. This is an abbreviated example; see [Order Event](https://docs.velafi.com/api-reference/webhooks/fiat-crypto-order-message-push.md) for every field.
>
> ```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` is `BUY` for on-ramp orders and `SELL` for off-ramp orders. Only some statuses are sent, and rejected or timed-out orders are sent as `70`. See [Order Event](https://docs.velafi.com/api-reference/webhooks/fiat-crypto-order-message-push.md) for the statuses that are sent.

> [accordion] GLOBAL\_PAYMENT\_WEBHOOK — fiat-to-fiat orders
>
> VelaFi sends this payload on status changes for fiat-to-fiat global payment orders created through the API.
>
> ```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` uses the same codes as the [Global Payments](https://docs.velafi.com/docs/guides/global-payments.md) order detail endpoint, but only the statuses listed on [Global Payment Event](https://docs.velafi.com/api-reference/webhooks/fiat-fiat-order-message-push.md) are sent.

> [accordion] FUNDING\_WEBHOOK — merchant funding events
>
> VelaFi sends this payload when a fiat deposit, fiat withdrawal, crypto purchase, or internal transfer on your merchant's fiat (Pending Fund) account is completed, canceled, or refunded. Crypto wallet withdrawals are covered by `CRYPTO_WITHDRAW_WEBHOOK` instead.
>
> ```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": ""
> }
> ```
>
> The `type` field is one of `DEPOSIT`, `WITHDRAW`, `BUY_CRYPTO`, `TRANSFER_IN`, or `TRANSFER_OUT`. The `status` field is `2` (completed), `3` (canceled), or `4` (refunded). Pending records are not sent.

## Acknowledge receipt

Your endpoint must return an HTTP `2xx` response (for example `200`) with the following JSON body to confirm that the notification was received and processed successfully. If VelaFi does not receive a `SUCCESS` acknowledgement, for example because your endpoint did not respond within 6 seconds, it retries delivery, for three attempts in total. See [Delivery and Retries](https://docs.velafi.com/api-reference/webhooks/webhook-setup.md#delivery-and-retries) for the schedule, duplicate handling, and ordering.

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

> [!NOTE]
>
> Return `{"callbackStatus": "SUCCESS"}` as soon as you have verified the signature and persisted the event. Perform any downstream processing asynchronously to avoid timeouts that could trigger unnecessary retries.


## Related topics

- [Webhook Setup](https://docs.velafi.com/api-reference/webhooks/webhook-setup.md)
- [Delete a Webhook](https://docs.velafi.com/api-reference/webhooks/delete-a-webhook.md)
- [Create a Webhook](https://docs.velafi.com/api-reference/webhooks/create-webhook-endpoints.md)
- [List Webhooks](https://docs.velafi.com/api-reference/webhooks/get-webhook-endpoints.md)
- [Update a Webhook](https://docs.velafi.com/api-reference/webhooks/update-a-webhook.md)


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