Webhook de Actualización de Pagos
Para recibir notificaciones en tiempo real sobre los pagos con SP Cuvex, debes configurar un webhook en tu sistema.
Integración de Webhook
Para que tu sistema reciba notificaciones sobre transacciones en tiempo real, completa el siguiente proceso:
- Disponer de un endpoint de acceso público que reciba los eventos en formato JSON mediante peticiones POST.
- Generar un texto secreto, compartirlo con nosotros y almacenarlo de manera segura. Este texto se usará para la firma de cada petición entrante.
- Con cada evento recibido en el webhook, verificar su autenticidad usando el texto secreto y la firma incluida en los encabezados de la petición.
- Luego de verificar la firma, responder de manera inmediata.
Endpoint
Crea un endpoint público al cual enviaremos notificaciones de eventos en formato JSON mediante peticiones POST.
En el encabezado x-sign enviaremos una firma generada con el algoritmo HMAC-SHA256, utilizando el texto secreto previamente compartido y el cuerpo (payload) de la petición. Esta firma permite asegurar que la petición ha sido enviada por SP Cuvex.
Texto secreto
Durante tu proceso de enrolamiento, debes compartir tanto la URL del endpoint como un texto secreto con nosotros.
Este texto será utilizado para generar la firma HMAC-SHA256 de cada petición. Debe ser alfanumérico, aleatorio, de alta entropía y con una longitud máxima de 128 caracteres.
Guárdalo de manera segura en tu servidor. No lo incluyas nunca en el código fuente ni en repositorios (Git, etc.).
Verificación de firma
En cada petición recibida, también se incluirá un encabezado x-sign, similar al siguiente:
sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
Esta firma será generada por nosotros con el algoritmo:
HMAC-SHA256(UTF-8(secret), body)
Debes replicar el cálculo de la firma utilizando el texto secreto almacenado de manera segura y el payload recibido.
- Usa UTF-8 para la conversión entre texto y bytes.
- Compara la firma recibida con la generada localmente mediante comparación segura contra timing.
- Recuerda omitir el prefijo
sha256=antes de comparar. - Si las firmas no coinciden, responde con HTTP 401.
Ejemplos
- Java (Spring Boot)
- Python (Flask)
- Node.js (Express)
@RestController
public class WebhookController {
@PostMapping(value = "/webhook", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> webhookEndpoint(HttpServletRequest request) throws IOException,
NoSuchAlgorithmException,
InvalidKeyException {
String signature = request.getHeader("x-sign");
byte[] body = this.getPayloadBytes(request);
this.validateSignature(signature, body);
// Do your data processing asynchronously to respond ASAP
return ResponseEntity.ok().build();
}
public byte[] getPayloadBytes(HttpServletRequest request) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
InputStream inputStream = request.getInputStream();
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
return outputStream.toByteArray();
}
public void validateSignature(final String receivedSignatue, final byte[] body) throws NoSuchAlgorithmException, InvalidKeyException {
HexFormat hexFormatter = HexFormat.of();
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
String keyString = "Sup3rS4cretT3xt!";
String cleanedReceivedSign = receivedSignatue.replace("sha256=", "");
byte[] receivedHashBytes = hexFormatter.parseHex(cleanedReceivedSign);
SecretKeySpec secretKey = new SecretKeySpec(keyString.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
hmacSha256.init(secretKey);
byte[] calculatedHash = hmacSha256.doFinal(body);
if (!Arrays.equals(calculatedHash, receivedHashBytes)) {
// Add an exception/handler to respond with HTTP Status 401 in this case.
// For any other cases, respond with HTTP Status 500
throw new RuntimeException("Signatures don't match");
}
}
}
from flask import Flask, request
import hmac
import hashlib
import binascii
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook_endpoint():
try:
signature = request.headers.get('x-sign')
body = request.get_data()
validate_signature(signature, body)
# Do your data processing asynchronously to respond ASAP
return '', 200
except ValueError:
return '', 401
except Exception:
return '', 500
def validate_signature(received_signature, body):
secret_key = "Sup3rS4cretT3xt!"
cleaned_received_sign = received_signature.replace("sha256=", "")
try:
received_hash_bytes = binascii.unhexlify(cleaned_received_sign)
except (ValueError, binascii.Error):
raise ValueError("Invalid signature format")
calculated_hash = hmac.new(
secret_key.encode('utf-8'),
body,
hashlib.sha256
).digest()
if not hmac.compare_digest(calculated_hash, received_hash_bytes):
raise ValueError("Signatures don't match")
if __name__ == '__main__':
app.run(debug=True, port=8080)
const express = require('express');
const crypto = require('crypto');
const app = express();
const PORT = 8080;
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
try {
const signature = req.headers['x-sign'];
validateSignature(signature, req.body);
// Do your data processing asynchronously to respond ASAP
return res.status(200).end();
} catch (error) {
if (error.message === "Signatures don't match" || error.message === "Invalid signature format") {
return res.status(401).end();
}
return res.status(500).end();
}
});
function validateSignature(receivedSignature, body) {
const secretKey = "Sup3rS4cretT3xt!";
const cleanedReceivedSign = receivedSignature.replace("sha256=", "");
let receivedHashBuffer = Buffer.from(cleanedReceivedSign, 'hex');
const calculatedHash = crypto
.createHmac('sha256', secretKey)
.update(body)
.digest();
if (!crypto.timingSafeEqual(calculatedHash, receivedHashBuffer)) {
throw new Error("Signatures don't match");
}
}
app.listen(PORT, () => {
console.log(`Webhook server running on http://localhost:${PORT}`);
});
Tratamiento de datos entrantes y respuesta
Después de verificar la firma, responde de inmediato.
Procesa el trabajo pesado de forma asíncrona en tu sistema.
Respuestas esperadas (sin payload):
- 200: firma verificada.
- 401: firma no válida.
- 500: error inesperado.
Anticlonado (replay protection)
Incluimos x-timestamp (UTC, segundos) y x-id (nonce único por evento).
Rechaza peticiones con x-timestamp fuera de una ventana ±5 minutos y x-id repetidos.
Reintentos
Si tu endpoint responde ≠ 2xx reintentaremos con backoff exponencial (p. ej., hasta 5 intentos en ~5 min).
Deduplica por x-id.
Definición de eventos
Pago creado
Este evento será enviado cada vez que se cree un nuevo pago.
{
"event": "PAYMENT_CREATED",
"data": {
"id": "fca84a27-2a4c-413c-9f0d-edff3c25959e",
"reference": "INV-09-2025-0001",
"network": "TRON",
"token": "USDT",
"amount": "5.25",
"confirmed_amount": "0",
"receiver": "TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz",
"status": "OPEN",
"valid_until": 1755532150,
"created_at": "2024-04-16T17:44:51Z",
"updated_at": "2024-04-16T17:44:51Z"
}
}
Campos:
id: string. Obligatorio. Identificador del pago.reference: string. Obligatorio. Identificador a nivel de tu sistema.network: string. Obligatorio. Red en la que se espera el pago.token: string. Obligatorio. Token con el cual se espera el pago.amount: string. Obligatorio. Cantidad a pagar.confirmed_amount: string. Obligatorio. Monto confirmado del pago.receiver: string. Obligatorio. Dirección de la wallet que recibirá el pago.status: string. Obligatorio. Estado actual del pago.valid_until: integer. Obligatorio. Timestamp de vencimiento del pago.created_at: string. Obligatorio. Fecha y hora en UTC de la creación (ISO 8601).updated_at: string. Obligatorio. Fecha y hora en UTC de la última modificación (ISO 8601).
Pago finalizado
Evento enviado cuando un pago es confirmado en la blockchain.
{
"event": "PAYMENT_FINISHED",
"data": {
"id": "fca84a27-2a4c-413c-9f0d-edff3c25959e",
"reference": "INV-09-2025-0001",
"network": "TRON",
"token": "USDT",
"amount": "5.25",
"confirmed_amount": "5.25",
"receiver": "TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz",
"status": "FINISHED",
"valid_until": 1755532150,
"created_at": "2024-04-16T17:44:51Z",
"updated_at": "2024-04-16T17:46:12Z",
"txs": [
{
"tx": "6e3a8368bcc7113cef086156fc4500b3322fa258781652cdc56a855c12e5cb3b",
"block_number": "75279138",
"sender_address": "TVAKUM28o1pNj6pkcckvWHFUs1foRcSwAt",
"amount": "123.456789"
}
]
}
}
Campos adicionales:
txs: array de objetos (opcional). Lista de transacciones que confirman el pago.
Cada objeto entxscontiene:tx: string. Hash de la transacción.block_number: string. Número de bloque.sender_address: string. Dirección emisora.amount: string. Monto de la transacción.
Pago extemporáneo
Evento enviado cuando un pago es confirmado pero fuera del tiempo de espera establecido.
{
"event": "PAYMENT_LATE_FINISHED",
"data": {
"id": "fca84a27-2a4c-413c-9f0d-edff3c25959e",
"reference": "INV-09-2025-0001",
"network": "TRON",
"token": "USDT",
"amount": "5.25",
"confirmed_amount": "5.25",
"receiver": "TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz",
"status": "LATE_PAYMENT",
"valid_until": 1755532150,
"created_at": "2024-04-16T17:44:51Z",
"updated_at": "2024-04-16T17:46:12Z",
"txs": [
{
"tx": "6e3a8368bcc7113cef086156fc4500b3322fa258781652cdc56a855c12e5cb3b",
"block_number": "75279138",
"sender_address": "TVAKUM28o1pNj6pkcckvWHFUs1foRcSwAt",
"amount": "123.456789"
}
]
}
}
Pago expirado
Evento enviado cuando el tiempo de espera expira sin recibir confirmación de pago.
{
"event": "PAYMENT_EXPIRED",
"data": {
"id": "fca84a27-2a4c-413c-9f0d-edff3c25959e",
"reference": "INV-09-2025-0001",
"network": "TRON",
"token": "USDT",
"amount": "5.25",
"confirmed_amount": "0",
"receiver": "TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz",
"status": "EXPIRED",
"valid_until": 1755532150,
"created_at": "2024-04-16T17:44:51Z",
"updated_at": "2024-04-16T17:44:51Z"
}
}
Pago fallido
Evento enviado si ocurre un error durante el procesamiento del pago.
{
"event": "PAYMENT_FAILED",
"data": {
"id": "fca84a27-2a4c-413c-9f0d-edff3c25959e",
"reference": "INV-09-2025-0001",
"network": "TRON",
"token": "USDT",
"error_message": "The network is offline",
"amount": "5.25",
"confirmed_amount": "0",
"receiver": "TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz",
"status": "FAILED",
"valid_until": 1755532150,
"created_at": "2024-04-16T17:44:51Z",
"updated_at": "2024-04-16T17:44:51Z"
}
}
Campo adicional:
error_message: string. Mensaje o descripción del error ocurrido.