Guía para desarrolladores
Sitio de VEXPayReferencia de API
Explorar documentación

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

  1. Créalo

    En Portal → Webhooks → New endpoint, o con POST /v1/webhooks. Lista solo los eventos que manejas.

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

  3. 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 evento notification.test.

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

SDK Nodejavascript
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);
});
Sin SDK (Node)javascript
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.completed verificado 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:

  1. Encuentra las entregas fallidas

    Portal → Webhooks → Delivery history muestra cada entrega con su estado, código HTTP y el inicio de tu respuesta.

  2. Consulta el objeto

    GET /v1/payments/:id, o GET /v1/payments/by-ref/:externalRef cuando 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.

  3. 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 su id.

  4. Hazlo periódicamente

    Un proceso periódico que revisa contra la API los pedidos que siguen "esperando pago" cierra la brecha para siempre.

Consultar por tu referenciabash
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íntomaCausa probableQué hacer
La firma nunca coincideEl cuerpo se parseó y se volvió a serializar antes de verificar, el secreto equivocado, o un secreto Test en LiveVerifica los bytes crudos con el secreto de ese endpoint.
El SDK dice que el timestamp está fuera de la toleranciaEl reloj del servidor está desfasadoSincroniza tu reloj (NTP).
Entrega fallida con un código 3xxTu URL redirigeRegistra la URL final.
Entrega fallida por tiempo agotadoTu endpoint tardó más de 10 segundosGuarda el evento, responde 2xx y luego procesa.
Entrega fallida con "blocked"El host resuelve a una dirección privadaCorrige 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ídoAgrégalo con PATCH /v1/webhooks/:id y recupéralo como se explica arriba.
429 al enviar pruebasMás de 10 pruebas en 10 minutosEspera unos minutos.

Relacionado: Webhooks · Concilia tu saldo · Idempotencia