Webhooks
Reciba notificaciones en tiempo real cuando los pagos se completen, fallen o estén pendientes.
Descripción General
Los Webhooks permiten que su servidor reciba notificaciones HTTP POST automáticas cuando ocurren eventos en su cuenta de Fivo. En lugar de consultar la API periódicamente, usted registra una URL y Fivo le envía los datos del evento a medida que ocurren.
Cada solicitud de webhook está firmada con HMAC-SHA256 para que pueda verificar que proviene de Fivo.
Configuración
Configure los webhooks desde su página de :Panel → Webhooks
- Pulse Añadir Webhook
- Introduzca la URL de su endpoint (debe ser HTTPS)
- Seleccione a qué eventos desea suscribirse
- Guarde: Fivo genera un secreto de firma para usted
Requisitos
- La URL debe usar HTTPS
- La URL no puede apuntar a direcciones IP privadas/internas (protección SSRF)
- Su endpoint debe responder con un código 2xx en un plazo de 5 segundos
Eventos
| Evento | Descripción |
|---|---|
payment.completed | El pago fue confirmado on-chain y acreditado en su wallet |
payment.failed | El pago falló (transacción revertida o expirada) |
refund.created | Se ha creado un reembolso para un pago |
refund.completed | Reembolso completado on-chain y enviado al cliente |
refund.failed | El reembolso falló (consulte failure_reason en data) |
Carga Útil
Cada webhook envía una carga útil JSON con el tipo de evento, una marca de tiempo y los datos del evento:
Eventos de Pago
{
"event": "payment.completed",
"timestamp": "2026-03-04T10:30:45.123Z",
"data": {
"payment_id": "fivo_live_abc123",
"amount": "100.00",
"currency": "USDC",
"status": "completed",
"tx_hash": "0xabc...def",
"from_address": "0x1234...5678",
"to_address": "0xabcd...ef01",
"source_chain": "ETH",
"destination_chain": "BASE",
"is_cross_chain": true,
"merchant_transfer_tx_hash": "0x9876...5432",
"customer_email": "customer@example.com",
"reference": "ORD-2026-001",
"metadata": { "product_id": "plan_pro" }
}
}Eventos de Reembolso
{
"event": "refund.completed",
"timestamp": "2026-03-04T10:35:20.456Z",
"data": {
"refund_id": "refund-uuid",
"payment_id": "payment-uuid",
"amount": "49.99",
"currency": "USDC",
"status": "completed",
"destination_address": "0xCustomer...",
"blockchain": "BASE",
"reason": "requested_by_customer",
"tx_hash": "0xabc..."
}
}Campos de Datos de Pago
objeto data (eventos de pago)
payment_idstringRequiredID de pago de Fivo
amountstringRequiredMonto del pago (cadena decimal)
currencystringRequiredSímbolo del token: USDC o EURC
statusstringRequiredEstado del pago: pending, completed o failed
tx_hashstring | nullRequiredHash de la transacción on-chain
from_addressstring | nullRequiredDirección de la wallet del pagador
to_addressstring | nullRequiredDirección de la wallet del comerciante
source_chainstring | nullRequiredBlockchain de origen (ej. ETH, BASE)
destination_chainstring | nullRequiredBlockchain de destino (null para la misma cadena)
is_cross_chainbooleanRequiredSi el pago fue transferido entre cadenas
merchant_transfer_tx_hashstringOptionalHash TX de la transferencia a la wallet del comerciante (solo entre cadenas)
customer_emailstring | nullOptionalCorreo electrónico del cliente si se proporcionó durante el pago
referencestring | nullOptionalReferencia del comerciante (ej. ID de pedido)
metadataobject | nullOptionalMetadatos personalizados adjuntos al pago
| Name | Type | Required | Description |
|---|---|---|---|
payment_id | string | Required | ID de pago de Fivo |
amount | string | Required | Monto del pago (cadena decimal) |
currency | string | Required | Símbolo del token: USDC o EURC |
status | string | Required | Estado del pago: pending, completed o failed |
tx_hash | string | null | Required | Hash de la transacción on-chain |
from_address | string | null | Required | Dirección de la wallet del pagador |
to_address | string | null | Required | Dirección de la wallet del comerciante |
source_chain | string | null | Required | Blockchain de origen (ej. ETH, BASE) |
destination_chain | string | null | Required | Blockchain de destino (null para la misma cadena) |
is_cross_chain | boolean | Required | Si el pago fue transferido entre cadenas |
merchant_transfer_tx_hash | string | Optional | Hash TX de la transferencia a la wallet del comerciante (solo entre cadenas) |
customer_email | string | null | Optional | Correo electrónico del cliente si se proporcionó durante el pago |
reference | string | null | Optional | Referencia del comerciante (ej. ID de pedido) |
metadata | object | null | Optional | Metadatos personalizados adjuntos al pago |
Cabeceras de la Solicitud
Fivo incluye estas cabeceras en cada solicitud de webhook:
Cabeceras
X-Fivo-SignaturestringRequiredFirma HMAC-SHA256 con prefijo sha256= (consulte Verificación de Firmas)
X-Fivo-EventstringRequiredTipo de evento (ej. payment.completed)
X-Fivo-TimestampstringRequiredMarca de tiempo Unix en segundos del momento en que se envió la solicitud
X-Fivo-TeststringOptionalSe establece como "true" para webhooks de prueba enviados desde el panel
| Name | Type | Required | Description |
|---|---|---|---|
X-Fivo-Signature | string | Required | Firma HMAC-SHA256 con prefijo sha256= (consulte Verificación de Firmas) |
X-Fivo-Event | string | Required | Tipo de evento (ej. payment.completed) |
X-Fivo-Timestamp | string | Required | Marca de tiempo Unix en segundos del momento en que se envió la solicitud |
X-Fivo-Test | string | Optional | Se establece como "true" para webhooks de prueba enviados desde el panel |
Verificación de Firmas
Verifique siempre la cabecera para confirmar que la solicitud proviene de Fivo. La firma se calcula como usando su secreto de webhook como clave, con el prefijo .X-Fivo-SignatureHMAC-SHA256(timestamp + "." + body)sha256=.
Su secreto de webhook tiene este aspecto: whsec_a1b2c3d4e5f6...
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const signature = req.headers['x-fivo-signature'];
const timestamp = req.headers['x-fivo-timestamp'];
const body = JSON.stringify(req.body);
// 1. Reject if timestamp is older than 5 minutes (replay protection)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 5 * 60) {
throw new Error('Webhook timestamp too old');
}
// 2. Compute expected signature: HMAC-SHA256(timestamp + "." + body)
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + body)
.digest('hex');
// 3. Compare using timing-safe comparison
if (signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
throw new Error('Invalid webhook signature');
}
return JSON.parse(body);
}Comparación segura en tiempo
crypto.timingSafeEqual()===Reintentos y Desactivación Automática
Si su endpoint devuelve un código no-2xx o se agota el tiempo, Fivo incrementa un contador de fallos. Tras fallos consecutivos, el webhook se desactiva automáticamente para evitar más entregas fallidas.10
Puede reactivar un webhook desactivado desde el panel en cualquier momento. El contador de fallos se reinicia con cualquier entrega exitosa.
Pruebas
Use el botón en su página de para enviar un evento de prueba a su endpoint. Los webhooks de prueba incluyen la cabecera para que su servidor pueda distinguirlos de los eventos reales.Enviar pruebaWebhooksX-Fivo-Test: true
Los registros de entrega son visibles en la página de detalle del webhook: puede inspeccionar la carga útil, el código de respuesta y el tiempo de respuesta de cada entrega.
Ejemplo Completo (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.FIVO_WEBHOOK_SECRET;
app.post('/webhooks/fivo', (req, res) => {
try {
// 1. Verify signature
const signature = req.headers['x-fivo-signature'];
const timestamp = req.headers['x-fivo-timestamp'];
const body = JSON.stringify(req.body);
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(timestamp + '.' + body)
.digest('hex');
if (signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Replay protection
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 5 * 60) {
return res.status(401).json({ error: 'Timestamp too old' });
}
// 3. Handle event
const { event, data } = req.body;
switch (event) {
case 'payment.completed':
console.log('Payment received:', data.amount, data.currency);
console.log('From:', data.from_address, 'on', data.source_chain);
console.log('Customer:', data.customer_email);
console.log('Reference:', data.reference);
// Update your order status, send confirmation, etc.
break;
case 'payment.failed':
console.log('Payment failed:', data.payment_id);
break;
case 'refund.created':
console.log('Refund created:', data.refund_id);
break;
case 'refund.completed':
console.log('Refund completed:', data.refund_id);
break;
case 'refund.failed':
console.log('Refund failed:', data.refund_id);
break;
}
res.status(200).json({ received: true });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
app.listen(3000);