Ir al contenido

Webhooks

Te avisamos de todo lo asíncrono con un POST a tu webhookUrl. El webhook es la fuente de verdad del desenlace, no la respuesta de POST /payouts.

eventType Cuándo
payment.settled La dispersión quedó firme: el beneficiario tiene sus Bs
payment.failed La dispersión no se ejecutó. El asiento ya está revertido
payment.reversed El banco devolvió una dispersión ya liquidada. Los fondos vuelven a tu saldo

Sale una sola vez por dispersión.

{ "id": "40545", "eventType": "payment.settled", "attempt": 1,
"payload": { "paymentId": "89920401-26da-4f69-b57d-bc20e0366ce2",
"asset": "BS", "amount": "5000000",
"bankReference": "012345678901", "method": "pago_movil" } }
Campo Qué es
id Id del evento. Deduplica por esto
eventType El tipo de la tabla de arriba
payload Detalle del hecho. Ver el aviso de abajo
attempt Número de intento de entrega. 1 la primera vez
POST /webhooks/dispersion HTTP/1.1
content-type: application/json
x-signature: sha256=<HMAC-SHA256(tu whsec, "<x-timestamp>.<cuerpo crudo>")>
x-timestamp: 1785270095123
x-event-type: payment.settled
x-event-id: 40545
x-delivery-attempt: 1
Node
const ts = req.headers['x-timestamp'];
const raw = req.body; // los BYTES exactos, no el objeto reserializado
const esperado = crypto.createHmac('sha256', WHSEC)
.update(`${ts}.${raw}`)
.digest('hex');
const firma = String(req.headers['x-signature']).replace('sha256=', '');
const valida = crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(firma));
// Anti-replay: rechaza si el timestamp tiene más de 5 minutos
const fresco = Math.abs(Date.now() - Number(ts)) < 300_000;
if (!valida || !fresco) return res.sendStatus(401);
Algoritmo HMAC-SHA256 sobre "<x-timestamp>.<cuerpo crudo>", en hexadecimal
Secreto Uno por programa
Ventana anti-replay ±5 minutos. La aplica tu receptor: nosotros firmamos con el timestamp, tú decides rechazar lo viejo
Comparación En tiempo constante (timingSafeEqual), nunca con ===

La rotación del secreto la hacemos nosotros. Guárdalo con el mismo cuidado que la API key.

  1. Verifica la firma y la frescura antes de mirar el cuerpo.

  2. Deduplica por id. La entrega es at-least-once: el mismo evento puede llegar dos veces y tu handler tiene que ser idempotente. El id también viaja en x-event-id.

  3. Responde 2xx rápido y procesa en segundo plano. Cualquier respuesta que no sea 2xx cuenta como fallo, y también un timeout: cortamos a los 5 segundos.

  4. Lee el estado completo con GET /v1/payments/{id} antes de cerrar la operación en tu contabilidad.

Reintentamos con espera creciente, hasta 12 intentos:

Intento Espera desde el anterior
2 5 s
3 15 s
4 45 s
5 2 min 15 s
6 6 min 45 s
7 20 min 15 s
8 ~1 h
9 ~3 h
10 – 12 6 h

Agotados los intentos, el evento pasa a status: "dead" — tu cola de fallidos. No se pierde ni se marca como entregado: es consultable y reentregable.

Ventana de terminal
# Lo que no llegó
curl -s "$BASE/v1/events?status=dead" -H "x-api-key: $KEY"
# Intento por intento, con su código HTTP y su latencia
curl -s "$BASE/v1/events/40545" -H "x-api-key: $KEY"
# Reentregar uno, o toda la cola
curl -s -X POST "$BASE/v1/events/40545/retry" -H "x-api-key: $KEY"
curl -s -X POST "$BASE/v1/events/retry-dead" -H "x-api-key: $KEY"
GET /v1/events/40545
{
"id": "40545", "event_type": "payment.settled",
"aggregate_type": "payment", "aggregate_id": "89920401-26da-4f69-b57d-bc20e0366ce2",
"payload": { "paymentId": "89920401-26da-4f69-b57d-bc20e0366ce2", "asset": "BS",
"amount": "5000000", "bankReference": "012345678901", "method": "pago_movil" },
"status": "published", "attempts": 3, "last_error": null, "next_attempt_at": null,
"published_at": "2026-08-31T19:06:52.004Z", "dead_at": null,
"created_at": "2026-08-31T19:06:44.120Z",
"deliveries": [
{ "attempt": 1, "status_code": 500, "ok": false, "error": "el receptor respondió 500", "duration_ms": 42 },
{ "attempt": 2, "status_code": null, "ok": false, "error": "timeout", "duration_ms": 5001 },
{ "attempt": 3, "status_code": 200, "ok": true, "error": null, "duration_ms": 87 }
]
}

POST /v1/events/retry-dead responde { "requeued": 7 }.

Ventana de terminal
curl -s "$BASE/v1/events?status=dead&eventType=payment.failed&limit=50" -H "x-api-key: $KEY"
Parámetro Valores
status pending · published · dead
eventType payment.settled · payment.failed · payment.reversed
limit · cursor Máximo 100. Aquí el cursor es el id numérico del evento

El listado no incluye payload; para verlo, pide el evento por su id.

POST /sandbox/webhooks/failing es un receptor que devuelve 500, y GET /sandbox/webhooks el sink con lo que te enviamos. Ver entorno de pruebas.