Webhooks salientes
Recibe notificaciones firmadas cuando ocurren eventos de negocio en tu cuenta.
vexorpay te notifica por HTTP cuando pasan eventos de negocio en tu cuenta. Configuras los webhooks desde Configuración → Webhooks en el dashboard: creas un endpoint con la URL de tu servidor, eliges a qué eventos suscribirte y vexorpay genera un secreto de firma (mostrado una sola vez).
Eventos
| Evento | Cuándo ocurre |
|---|---|
payment.completed | Polar cobró una venta y la acreditó en tu saldo de Polar. |
payment.failed | Un pago falló. |
payment.refunded | Una venta fue reembolsada. |
payment.disputed | Una venta fue disputada (chargeback). |
dispute.resolved | Una disputa se resolvió (ganada o perdida). |
payout.paid | Un retiro de Polar se acreditó en tu cuenta de cobro (hecho desde vexorpay o directamente en Polar). |
subscription.created | Se creó una suscripción nueva sobre un link recurrente. |
subscription.payment.completed | Polar cobró un ciclo de la suscripción. |
subscription.payment.failed | Falló el cobro de un ciclo de la suscripción. |
subscription.canceled | Se canceló una suscripción. |
subscription.trial_will_end | Está por terminar el período de prueba de una suscripción. |
Puedes suscribir cada endpoint a todos o a un subconjunto de estos eventos.
Entrega
- Cada evento se envía por
POSTal endpoint, conContent-Type: application/json. - Sin confirmación
2xx, vexorpay reintenta con backoff (~5 min, ~30 min, ~3 h) hasta 4 intentos y lo marca como fallido. EnConfiguración → Webhooksves el log de entregas (intentos, estado, último error). - Un mismo evento enviado a varios endpoints y en varios intentos comparte el mismo
evt_(id idempotente para el receptor).
Cuerpo del evento
{
"id": "evt_6c2314a4fb4a408c07c2",
"type": "payment.completed",
"created": "2026-01-02T12:05:00.000Z",
"sandbox": false,
"data": {
"charge": {
"id": "67f1c1e2c8a4b0d1f2e3a4b5",
"provider": "polar",
"reference": "ORD-1001",
"metadata": null,
"polarOrderId": "59f4a98b-7034-437d-92b7-f463758ba69f",
"polarCheckoutId": "8150f7d1-6404-48c2-8797-842e71a500ad",
"amountCents": 1900,
"currency": "usd",
"status": "paid",
"sandbox": false,
"paidAt": "2026-01-02T12:05:00.000Z"
},
"payment_link": {
"id": "66f1c1e2c8a4b0d1f2e3a4b5",
"slug": "a1b2c3d4e5",
"title": "Ebook UX en 30 días",
"reference": "CATALOGO-UX"
}
}
}data.charge es la venta de Polar: su id es el de GET /api/v1/orders/{id}, amountCents es el total que pagó el comprador y reference es tu referencia de esa venta (la que mandaste con ?reference= en el link o en el checkout por API; ver Identificar cada venta). La referencia del link va en payment_link.reference. sandbox viene a nivel raíz y, para payment.* y payout.paid, también dentro del objeto. Los eventos de venta agregan bloques extra según el caso:
payment.refunded→data.refund(id,amountCents).payment.disputed→data.dispute(status,reason,evidenceDeadline,amountCents).dispute.resolved→data.dispute(status:won/lost,amountCents,recoveredCents).
Las suscripciones (subscription.*) traen "provider": "polar" en data.subscription. payout.paid se emite también por los retiros que haces directamente en Polar (se detectan cada 10 minutos); data.payout trae "provider": "polar", source (vexorpay o polar) y el monto acreditado en tu moneda (accountCurrency, accountAmount). Ver Retiros.
Eventos de suscripción
Los eventos subscription.* traen el bloque subscription y payment_link:
{
"id": "evt_9a1b2c3d4e5f6a7b8c9d",
"type": "subscription.payment.completed",
"created": "2026-02-02T12:05:00.000Z",
"sandbox": false,
"data": {
"subscription": {
"id": "68f1c1e2c8a4b0d1f2e3a4b5",
"provider": "polar",
"status": "active",
"amountCents": 900,
"currency": "usd",
"interval": "month",
"intervalCount": 1,
"cancelAtPeriodEnd": false,
"customerEmail": "cliente@mail.com",
"sandbox": false,
"createdAt": "2026-01-02T12:00:00.000Z"
},
"payment_link": {
"id": "66f1c1e2c8a4b0d1f2e3a4b7",
"slug": "b2c3d4e5f6",
"title": "Membresía mensual",
"reference": null
},
"charge": {
"id": "67f1c1e2c8a4b0d1f2e3a4c9",
"amountCents": 900,
"currency": "usd",
"status": "paid",
"paidAt": "2026-02-02T12:05:00.000Z",
"sandbox": false
}
}
}subscription.payment.failed agrega data.payment_failure (amountCents, nextPaymentAttempt). Para payout.paid el data contiene payout en vez de charge/payment_link.
Verificación de firma
Cada request incluye:
| Header | Valor |
|---|---|
vexorpay-Signature | HMAC-SHA256 del cuerpo crudo (exactamente como llega) con tu secreto, en hex. |
vexorpay-Event-Id | Id del evento (evt_...), igual al id del body. |
Ejemplo
import { createHmac, timingSafeEqual } from 'node:crypto'
async function verify(req, secret) {
const raw = await text(req) // cuerpo crudo, SIN parsear
const sig = req.headers['vexorpay-signature']
const expected = createHmac('sha256', secret).update(raw).digest('hex')
const a = Buffer.from(sig ?? '', 'hex')
const b = Buffer.from(expected, 'hex')
const ok = a.length === b.length && timingSafeEqual(a, b)
if (ok) req.event = JSON.parse(raw) // recién acá parseás
}
// Uso:
// 1) verificás la firma
// 2) deduplicás por body.id (guardás el evt_ procesado)
// 3) recién entonces procesás y respondés 2xx- Nunca confíes en un POST sin firma válida.
- Compara en tiempo constante (
timingSafeEqual). - Parsea el body solo después de verificar; el hash usa el body crudo.
Verificación a demanda (reconciliación)
Si perdiste un webhook, puedes consultar el mismo evento en el histórico:
GET /v1/eventsCada evento del histórico tiene el mismo evt_ que se envía por webhook (mismos id, type y data) → puedes deduplicar y re-sincronizar lo que faltó.
Rotación y seguridad
- El secreto solo se muestra una vez al crearlo; si lo pierdes, rótalo desde el dashboard (el endpoint deja de funcionar hasta que actualices tu verificación).
- Puedes pausar un endpoint sin borrarlo (no recibe eventos mientras está pausado).
- Guarda el secreto como variable de entorno, nunca en el frontend ni en repositorios.