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

# Configuración de webhooks

> Configura tu servidor para recibir eventos de webhook de VelaFi y validar las firmas RSA-SHA256 del payload, con código de ejemplo en Java, Python y JavaScript.

VelaFi envía notificaciones de eventos a la URL que registraste como solicitudes HTTP POST con payloads JSON. Para proteger tu endpoint de solicitudes falsificadas, VelaFi firma cada payload de webhook con RSA-SHA256 e incluye la firma codificada en hexadecimal en el encabezado de la solicitud `signature`. Tu servidor debe verificar esta firma antes de procesar el evento.

## Cómo funciona la verificación de la firma

**1. Extrae el encabezado de la firma**

Lee el encabezado `signature` de la solicitud HTTP entrante. El valor es la firma RSA-SHA256 del cuerpo sin procesar de la solicitud, codificada como una cadena hexadecimal.

**2. Carga tu clave pública**

Obtén la `publicKey` que se devolvió al crear el endpoint del webhook. También se incluye para cada webhook en la respuesta de [Listar webhooks](https://docs.velafi.com/es/api-reference/webhooks/get-webhook-endpoints.md). Es una clave pública RSA codificada en Base64 en formato X.509.

**3. Calcula y verifica la firma**

Usa RSA-SHA256 para verificar la firma con los bytes del cuerpo sin procesar de la solicitud y la clave pública. Decodifica la firma hexadecimal a bytes antes de verificarla.

**4. Responde a VelaFi**

Devuelve `{"callbackStatus": "SUCCESS"}` si la verificación es correcta, o `{"callbackStatus": "FAIL"}` si no lo es. VelaFi usa esta respuesta para determinar si debe reintentar la entrega.

> [!WARNING]
>
> Verifica siempre la firma antes de actuar sobre un payload de webhook. Procesar eventos no verificados puede exponer tu sistema a ataques de repetición y a cambios de estado fraudulentos.

## Código de verificación de la firma

> [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 {
>
>     // Coloca tu clave pública según el entorno
>     private String publicKey = "YOUR_PUBLIC_KEY";
>
>     public String callbackNotice(HttpServletRequest request, String param) {
>         // Paso 1: Extrae el encabezado de la firma
>         String signature = request.getHeader("signature");
>
>         // Paso 2: Verifica con RSA-SHA256 (firma codificada en hexadecimal)
>         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. Decodifica la clave pública codificada en Base64
>             byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyStr);
>             X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
>
>             // 2. Genera la clave pública RSA
>             KeyFactory keyFactory = KeyFactory.getInstance("RSA");
>             PublicKey publicKey = keyFactory.generatePublic(keySpec);
>
>             // 3. Inicializa el objeto Signature
>             Signature sig = Signature.getInstance("SHA256withRSA");
>             sig.initVerify(publicKey);
>
>             // 4. Actualiza los datos que se van a verificar
>             sig.update(data.getBytes("UTF-8"));
>
>             // 5. Decodifica la firma hexadecimal
>             byte[] signatureBytes = decodeHex(signature);
>
>             // 6. Verifica
>             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
>
> # Coloca tu clave pública según el entorno
> public_key_b64 = "YOUR_PUBLIC_KEY"
>
> def callback_notice():
>     # Paso 1: Extrae el cuerpo sin procesar y el encabezado de la firma
>     param = request.get_data(as_text=True)
>     signature_hex = request.headers.get('signature')
>
>     # Paso 2: Verifica la firma RSA-SHA256 (codificada en hexadecimal)
>     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:
>         # Decodifica la clave pública en Base64 e impórtala
>         pub_key_bytes = base64.b64decode(public_key_b64)
>         public_key = RSA.import_key(pub_key_bytes)
>
>         # Calcula el hash del mensaje
>         h = SHA256.new(data.encode("utf-8"))
>
>         # Decodifica la firma hexadecimal a bytes
>         sig_bytes = binascii.unhexlify(signature_hex)
>
>         # Verifica
>         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');
>
> // Coloca tu clave pública según el entorno
> // La clave debe estar en formato PEM, o puedes derivarla de la cadena Base64
> const publicKeyBase64 = "YOUR_PUBLIC_KEY";
>
> async function callbackNotice(req, param) {
>     // Paso 1: Extrae el encabezado de la firma
>     const signatureHex = req.headers['signature'];
>
>     // Paso 2: Verifica la firma RSA-SHA256 (codificada en hexadecimal)
>     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 {
>         // Convierte la clave pública Base64 a formato PEM
>         const pem = `-----BEGIN PUBLIC KEY-----\n${publicKeyBase64}\n-----END PUBLIC KEY-----`;
>
>         // Convierte la firma hexadecimal a Buffer
>         const signatureBuffer = Buffer.from(signatureHex, 'hex');
>
>         // Crea el verificador
>         const verify = crypto.createVerify('RSA-SHA256');
>         verify.update(data, 'utf8');
>
>         return verify.verify(pem, signatureBuffer);
>     } catch (err) {
>         console.error('Verification error:', err);
>         return false;
>     }
> }
> ```

## Tipos de eventos de webhook

VelaFi admite los siguientes tipos de eventos. Cada uno requiere registrar un endpoint de webhook por separado. La creación de un webhook se rechaza si ya existe un webhook activo para ese tipo de evento. Los webhooks pertenecen a tu cuenta de VelaFi, por lo que un webhook recibe su evento para todos los comercios de la cuenta:

> [card] ORDER\_WEBHOOK
>
> Se dispara cuando una orden de fiat a cripto o de cripto a fiat creada mediante la API cambia de estado.

> [card] GLOBAL\_PAYMENT\_WEBHOOK
>
> Se dispara cuando una orden de pago global de fiat a fiat creada mediante la API cambia de estado.

> [card] FUNDING\_WEBHOOK
>
> Se dispara cuando un registro de fondeo (un depósito, retiro, compra de cripto o transferencia interna) se completa, se cancela o se reembolsa.

> [card] ON\_RAMP\_WITHDRAW\_WEBHOOK
>
> Se dispara cuando el retiro de cripto de una orden on-ramp completada por API hacia la dirección de billetera de destino de la orden es exitoso.

> [card] STABLE\_COIN\_PAYMENT\_WEBHOOK
>
> Se dispara cuando se confirma una orden de pago con stablecoin.

> [card] [CRYPTO\_WITHDRAW\_WEBHOOK](https://docs.velafi.com/es/api-reference/webhooks/crypto-withdraw-message-push.md)
>
> Se dispara cuando un retiro de cripto desde una billetera del comercio es exitoso, falla o se cancela.

> [!TIP]
>
> Guarda de forma segura la `publicKey` que se devuelve al crear el webhook. La necesitas en cada verificación. Si la pierdes, llama a [Listar webhooks](https://docs.velafi.com/es/api-reference/webhooks/get-webhook-endpoints.md), que devuelve la `publicKey` de cada webhook.

## Entrega y reintentos

Cada entrega es un HTTP POST con un cuerpo JSON y un encabezado `signature`. Cada payload contiene estos campos además de los campos específicos del evento:

| Campo | Descripción |
| - | - |
| `webhookId` | ID del webhook para el que se puso en cola el evento. |
| `eventType` | El tipo de evento. |
| `timestamp` | Marca de tiempo Unix (milisegundos) en la que se firmó este intento de entrega. Se genera un valor nuevo en cada intento. |

Los valores del payload se envían como cadenas JSON, incluidos los IDs, los montos y las marcas de tiempo.

* **Confirmación de recepción.** Una entrega cuenta como exitosa solo cuando tu endpoint devuelve un estado HTTP `2xx` y un cuerpo con un objeto JSON cuyo `callbackStatus` es `SUCCESS`. Cualquier otro estado o cuerpo cuenta como un intento fallido. VelaFi usa tiempos de espera de 6 segundos para conexión, escritura y lectura, y un tiempo de espera agotado también cuenta como intento fallido.
* **Reintentos.** Una entrega fallida normalmente se reintenta de forma automática, con un total de tres intentos. El segundo intento se hace entre 5 y 10 minutos después del primer fallo, y el tercero entre 60 y 65 minutos después del segundo fallo. Después del tercer intento fallido, el evento ya no se reintenta. La Open API no tiene un endpoint para solicitar una nueva entrega.
* **Duplicados.** El mismo evento puede entregarse más de una vez. Por ejemplo, si tu endpoint procesa una entrega pero su respuesta no se recibe a tiempo, el evento se reintenta. Haz que tu handler sea idempotente usando como clave el tipo de evento, el ID del registro (`orderId`, `txId` o `withdrawId`) y el estado.
* **Orden y estados omitidos.** VelaFi no garantiza que las entregas lleguen en el orden en que ocurrieron los estados. Para `ORDER_WEBHOOK`, `GLOBAL_PAYMENT_WEBHOOK`, `FUNDING_WEBHOOK` y `ON_RAMP_WITHDRAW_WEBHOOK`, VelaFi revisa el estado actual del registro antes de cada intento. Si el estado cambió desde que el evento se puso en cola, ese evento se descarta y no se entrega, por lo que es posible que no recibas todos los estados intermedios. Consulta la orden o el registro de fondeo para leer su estado más reciente. `CRYPTO_WITHDRAW_WEBHOOK` se envía solo para el estado final de un retiro.
* **Solo webhooks activos.** Los eventos se ponen en cola solo mientras exista un webhook activo (estado `10`) para el tipo de evento. Los eventos que ocurren mientras no hay un webhook activo no se ponen en cola y no se entregan después. Los eventos que ya estaban en cola no se cancelan cuando un webhook se desactiva o se elimina: se retienen y, si vuelve a existir un webhook activo para el mismo tipo de evento en un plazo aproximado de 15 días, se entregan a la URL de ese webhook con su `webhookId` original. Los eventos `CRYPTO_WITHDRAW_WEBHOOK` son la excepción: se entregan solo al webhook para el que se pusieron en cola, por lo que no se entregan a un webhook nuevo que crees después de eliminar o desactivar el anterior.


## Related topics

- [Crear un webhook](https://docs.velafi.com/es/api-reference/webhooks/create-webhook-endpoints.md)
- [Evento de retiro de on-ramp](https://docs.velafi.com/es/api-reference/webhooks/on-ramp-withdraw-message-push.md)
- [Evento de orden](https://docs.velafi.com/es/api-reference/webhooks/fiat-crypto-order-message-push.md)
- [Evento de fondeo](https://docs.velafi.com/es/api-reference/webhooks/funding-records-message-push.md)
- [Evento de pago global](https://docs.velafi.com/es/api-reference/webhooks/fiat-fiat-order-message-push.md)


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