Ir al contenido

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.

Variables de esta guía
BASE=https://qa.bolivarconnect.com # en producción, la URL de producción
KEY=sk_test_... # en producción, tu sk_live_
  1. 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 https y 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é. Scope tenant:write o webhooks:write.

  2. 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 key sk_live_.
  3. 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.test firmado igual que los reales. Si delivered es false, statusCode y error te 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 en GET /v1/events. Funciona igual en los dos entornos y no mueve dinero.

  4. 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.

  5. 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 error
    curl -s "$BASE/v1/events/40545" -H "x-api-key: $KEY"

    Scope events:read. status es pending (aún en reintentos), published (tu servidor respondió 2xx) o dead (se agotaron los 12 intentos).

  6. 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 fallidos
    curl -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.

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
Node (Express)
import crypto from 'node:crypto';
import express from 'express';
const WHSEC = process.env.BOLIVAR_WEBHOOK_SECRET; // el whsec_ de ESTE entorno, fuera del código
const 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')));
});

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-timestamp de hace más de 5 minutos.
  • Un mismo x-event-id dos veces se procesa una sola vez.
  • Responde 2xx en menos de 5 s.

Ver antes de producción.