Probar tu webhook
Esta guía deja tu receptor de webhooks probado y la sigues tú solo, con tu propia key. La secuencia es la misma en QA y en producción: solo cambian la URL base y el tipo de key. Para la referencia completa del formato, ver webhooks.
| QA (pruebas) | Producción | |
|---|---|---|
BASE |
https://qa.bolivarconnect.com |
La URL de producción que te dimos |
KEY |
sk_test_… (en QA solo hay keys de prueba) |
sk_live_… (en producción solo hay keys live) |
whsec_ |
El de QA | El de producción: lo obtienes en el paso 2 con tu key live |
El secreto de un entorno no sirve en el otro. Haz los pasos completos en QA y repítelos en producción con tu key live antes de tu primer pago.
BASE=https://qa.bolivarconnect.com # en producción, la URL de producciónKEY=sk_test_... # en producción, tu sk_live_-
Registra la URL
Sección titulada «Registra la URL»Ventana de terminal curl -s -X PUT $BASE/v1/tenant/webhook \-H "x-api-key: $KEY" -H 'content-type: application/json' \-d '{"url":"https://tu-dominio.com/webhooks/bolivar"}'Tiene que ser
httpsy pública. Para probar desde tu máquina, expón el receptor con un túnel (ngrok, Cloudflare Tunnel) y registra la URL que te dé. Scopetenant:writeowebhooks:write. -
Obtén o rota el secreto (
Sección titulada «Obtén o rota el secreto (whsec_)»whsec_)Si tu key te llegó por enlace, todavía no tienes el secreto: este paso te lo da. Si ya lo tienes, este mismo paso lo rota.
Ventana de terminal curl -s -X POST $BASE/v1/tenant/webhook-secret/rotate -H "x-api-key: $KEY"201 Created { "webhookSecret": "whsec_…","warning": "Úsalo para verificar la firma (x-signature = HMAC-SHA256 de `timestamp.body`)." }- Se muestra una sola vez. Guárdalo como guardas la API key, fuera del código.
- El anterior deja de valer en el acto: lo que salga desde ese momento, reintentos incluidos, va firmado con el nuevo. Pon el nuevo en tu receptor enseguida, o acepta las dos firmas mientras cambias.
- Scope
tenant:write. En producción se rota con tu keysk_live_.
-
Envía un evento de prueba
Sección titulada «Envía un evento de prueba»Ventana de terminal curl -s -X POST $BASE/v1/tenant/webhook/test -H "x-api-key: $KEY"200 OK { "url": "https://tu-dominio.com/webhooks/bolivar", "delivered": true,"statusCode": 200, "durationMs": 143, "error": null, "hint": "…" }Llega un
webhook.testfirmado igual que los reales. Sideliveredesfalse,statusCodeyerrorte dicen por qué (tu servidor respondió algo que no es 2xx, tardó más de 5 s, o la URL no es pública). Este evento no pasa por la cola: no se reintenta ni aparece enGET /v1/events. Funciona igual en los dos entornos y no mueve dinero. -
Recibe el primer evento real
Sección titulada «Recibe el primer evento real»Los eventos reales (
payment.settled,payment.failed,payment.reversed) salen cuando una dispersión llega a su desenlace. Se dispersa igual en los dos entornos: ver dispersar bolívares. -
Revisa lo que te enviamos
Sección titulada «Revisa lo que te enviamos»Ventana de terminal # Los últimos eventos (sin payload)curl -s "$BASE/v1/events?limit=10" -H "x-api-key: $KEY"# Uno, con su payload e intento por intento: código HTTP, latencia y errorcurl -s "$BASE/v1/events/40545" -H "x-api-key: $KEY"Scope
events:read.statusespending(aún en reintentos),published(tu servidor respondió 2xx) odead(se agotaron los 12 intentos). -
Reentrega y prueba los reintentos
Sección titulada «Reentrega y prueba los reintentos»Si tu receptor falló (respondió algo que no es 2xx o no respondió), lo ves crecer en
GET /v1/events/{id}. Cuando lo arregles:Ventana de terminal # Reentregar uno (también si ya llegó: sirve para probar tu deduplicación)curl -s -X POST "$BASE/v1/events/40545/retry" -H "x-api-key: $KEY"# Reentregar toda la cola de fallidoscurl -s -X POST "$BASE/v1/events/retry-dead" -H "x-api-key: $KEY"Scope
events:write. Reentregar no mueve dinero: solo vuelve a enviar el aviso. La espera entre intentos está en webhooks.
Verifica la firma en tu receptor
Sección titulada «Verifica la firma en tu receptor»Cada entrega trae estas cabeceras:
| Cabecera | Qué es |
|---|---|
x-signature |
sha256= + HMAC-SHA256 en hexadecimal de "<x-timestamp>.<cuerpo crudo>" con tu whsec_ |
x-timestamp |
Milisegundos desde 1970, como texto |
x-event-id |
Id del evento. Es el mismo id del cuerpo |
x-event-type |
payment.settled, payment.failed, payment.reversed o webhook.test |
x-delivery-attempt |
1 la primera vez |
import crypto from 'node:crypto';import express from 'express';
const WHSEC = process.env.BOLIVAR_WEBHOOK_SECRET; // el whsec_ de ESTE entorno, fuera del códigoconst vistos = new Set(); // en producción, tu base de datos
const app = express();// El cuerpo CRUDO: si parseas y reserializas el JSON, la firma no coincide.app.post('/webhooks/bolivar', express.raw({ type: 'application/json' }), (req, res) => { const ts = String(req.headers['x-timestamp'] ?? ''); const firma = String(req.headers['x-signature'] ?? '').replace(/^sha256=/, ''); const esperada = crypto.createHmac('sha256', WHSEC) .update(`${ts}.${req.body.toString('utf8')}`) .digest('hex');
// 1. Firma, en tiempo constante (timingSafeEqual exige el mismo largo). const a = Buffer.from(esperada, 'hex'); const b = Buffer.from(firma, 'hex'); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
// 2. Frescura: ±5 minutos. if (!Number.isFinite(Number(ts)) || Math.abs(Date.now() - Number(ts)) > 5 * 60_000) { return res.sendStatus(401); }
// 3. Deduplicación por x-event-id: la entrega es at-least-once. const id = String(req.headers['x-event-id']); if (vistos.has(id)) return res.sendStatus(200); vistos.add(id);
// 4. Responde 2xx en menos de 5 s y procesa aparte. res.sendStatus(200); encolar(JSON.parse(req.body.toString('utf8')));});Lista de comprobación
Sección titulada «Lista de comprobación»En QA con tu sk_test_, y otra vez en producción con tu sk_live_ antes del
primer pago:
- La URL está registrada (paso 1) y el
whsec_de este entorno está guardado aparte (paso 2). - El evento de prueba llega con
delivered: true(paso 3). - Tu receptor rechaza una firma incorrecta y un
x-timestampde hace más de 5 minutos. - Un mismo
x-event-iddos veces se procesa una sola vez. - Responde 2xx en menos de 5 s.
Ver antes de producción.