Configura, verifica y recupera webhooks
Registra un endpoint, verifica cada firma sobre el cuerpo crudo, elimina duplicados y recupera un evento que te perdiste: los webhooks se envían una vez, sin reintento.
Qué vas a hacer
VEXPay firma cada webhook y lo envía una vez, con un tiempo límite de 10 segundos y sin reintento automático. Tu endpoint debe verificar la firma, responder 2xx rápido e ignorar duplicados, y tu sistema debe poder consultar lo que se haya perdido. Hazlo una vez en Test y otra en Live: tienen endpoints y secretos separados.
- Una URL HTTPS pública. Se rechazan localhost, IPs privadas, nombres
*.internal/*.local, URLs con usuario o contraseña, y las redirecciones. - Acceso al cuerpo crudo de la solicitud en tu framework web (las firmas se calculan sobre los bytes exactos).
Registra el endpoint
- Créalo
En Portal → Webhooks → New endpoint, o con
POST /v1/webhooks. Lista solo los eventos que manejas. - Guarda el secreto
El secreto de firma se devuelve solo en la respuesta de creación. Guárdalo como
VEXPAY_WEBHOOK_SECRET. Si lo pierdes, crea un endpoint nuevo y elimina el anterior: el secreto no se puede cambiar después. - Envíate una prueba
Haz clic en Send test en el portal o llama a
POST /v1/notifications/test(hasta 10 cada 10 minutos). Recibes un eventonotification.test.
curl --request POST "${API_URL}/v1/webhooks" \
--header "content-type: application/json" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--data '{
"url": "https://shop.example/webhooks/vexpay",
"events": ["payment.completed", "payment.failed", "payment.canceled", "payment.reversed", "payout.completed", "payout.failed"]
}'
# Guarda ahora el "secret" de la respuesta.Cambia la URL, la lista de eventos o desactiva un endpoint con PATCH /v1/webhooks/:id (la lista de eventos se reemplaza completa). El catálogo completo de eventos está en Webhooks.
Verifica cada entrega
Cada entrega es un POST con cuerpo JSON { event, data, timestamp } y el encabezado VexPay-Signature: t=<segundos unix>,v1=<hex>. v1 es el HMAC-SHA256 de "<t>.<cuerpo crudo>" con el secreto de tu endpoint. Acepta la entrega cuando algún valor v1 coincide y t está a menos de 5 minutos de tu reloj.
app.post('/webhooks/vexpay', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = vexpay.webhooks.constructEvent(
req.body, // bytes crudos, no JSON parseado
req.headers['vexpay-signature'],
process.env.VEXPAY_WEBHOOK_SECRET,
);
} catch {
return res.sendStatus(400);
}
const eventId = req.headers['vexpay-event-id'];
if (await alreadyProcessed(eventId)) return res.sendStatus(200);
await queue.add({ eventId, event }); // haz el trabajo después de responder
res.sendStatus(200);
});import crypto from 'node:crypto';
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = header.split(',').map((p) => p.split('='));
const t = Number(parts.find(([k]) => k === 't')?.[1]);
const signatures = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return signatures.some(
(s) => s.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)),
);
}- Elimina duplicados con el encabezado
VexPay-Event-Id: el mismo evento puede llegarte más de una vez. - Responde 2xx en menos de 10 segundos y haz el trabajo lento en una cola. Un tiempo agotado cuenta como entrega fallida y no se reintenta.
- Los eventos pueden llegar desordenados. Decide según el estado actual del objeto, no según el orden de los eventos.
- Entrega el pedido solo con un
payment.completedverificado o con un GET que muestre el pago completado. Las redirecciones del navegador son pistas, nunca prueba.
Recupera un evento que te perdiste
Si tu endpoint estaba caído o respondió lento, el evento no se enviará de nuevo. Consulta el objeto en su lugar:
- Encuentra las entregas fallidas
Portal → Webhooks → Delivery history muestra cada entrega con su estado, código HTTP y el inicio de tu respuesta.
- Consulta el objeto
GET /v1/payments/:id, oGET /v1/payments/by-ref/:externalRefcuando nunca recibiste un id. COP:GET /v1/cop/payments/:id. Checkout:GET /v1/checkout/sessions/:id. Payouts:GET /v1/payouts/:id,GET /v1/payouts/by-ref/:externalRef,GET /v1/crypto/payouts/:id. - Ponte al día con los movimientos
Recorre
GET /v1/balance/transactions(bolívares) desde tu último movimiento procesado, y registra cada movimiento una sola vez por suid. - Hazlo periódicamente
Un proceso periódico que revisa contra la API los pedidos que siguen "esperando pago" cierra la brecha para siempre.
curl "${API_URL}/v1/payments/by-ref/order-1042" \
--header "x-api-key: ${VEXPAY_API_KEY}"Comprueba que funcionó
- Send test aparece como entrega exitosa con tu código 2xx en Delivery history.
- Tu endpoint rechaza una entrega con el cuerpo modificado o con un secreto equivocado.
- Reenviar la misma entrega no procesa el pedido dos veces.
Si algo sale mal
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| La firma nunca coincide | El cuerpo se parseó y se volvió a serializar antes de verificar, el secreto equivocado, o un secreto Test en Live | Verifica los bytes crudos con el secreto de ese endpoint. |
| El SDK dice que el timestamp está fuera de la tolerancia | El reloj del servidor está desfasado | Sincroniza tu reloj (NTP). |
| Entrega fallida con un código 3xx | Tu URL redirige | Registra la URL final. |
| Entrega fallida por tiempo agotado | Tu endpoint tardó más de 10 segundos | Guarda el evento, responde 2xx y luego procesa. |
| Entrega fallida con "blocked" | El host resuelve a una dirección privada | Corrige el registro DNS; las entregas bloqueadas no se reintentan. |
| Un evento nunca llegó | No está en la lista de eventos del endpoint, o el endpoint estaba caído | Agrégalo con PATCH /v1/webhooks/:id y recupéralo como se explica arriba. |
| 429 al enviar pruebas | Más de 10 pruebas en 10 minutos | Espera unos minutos. |
Relacionado: Webhooks · Concilia tu saldo · Idempotencia