Webhooks
Hay dos flujos de webhooks distintos en Rekurra: los que Rekurra te envía a ti (para que reacciones a cambios en suscripciones y cobros), y el ingress de Wompi hacia Rekurra (interno, informativo para entender el pipeline completo).
Webhooks que Rekurra te envía
1. Registra un endpoint
curl -X POST https://api.rekurra.dev/v1/webhook-endpoints \
-H "Authorization: Bearer rk_live_XXXXXXXXXXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tuapp.com/webhooks/rekurra",
"events": ["subscription.created", "charge.approved", "charge.declined", "subscription.canceled"]
}'{
"id": "we_01HZXK7QJ5M8Z9X2R3TVQ7A1B2",
"url": "https://tuapp.com/webhooks/rekurra",
"secret": "whsec_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"events": ["subscription.created", "charge.approved", "charge.declined", "subscription.canceled"],
"active": true
}El secret se muestra una sola vez, en la respuesta de creación.
Guárdalo de forma segura — lo necesitas para verificar la firma de cada
entrega.
2. Verifica la firma Rekurra-Signature
Cada entrega incluye el header:
Rekurra-Signature: t=1735142400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdDonde v1 = hmac_sha256(secret, t + "." + body) sobre el cuerpo crudo
de la petición.
const crypto = require("crypto");
function verificarFirma(rawBody, header, secret) {
const [tPart, v1Part] = header.split(",");
const t = tPart.split("=")[1];
const v1 = v1Part.split("=")[1];
const payload = `${t}.${rawBody}`;
const esperado = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperado));
}3. Eventos disponibles
Los eventos que puedes suscribir:
subscription.createdsubscription.canceledsubscription.pausedsubscription.resumedsubscription.past_duesubscription.plan_changedcharge.approvedcharge.declined
Todos los eventos históricos quedan también disponibles vía API → Events para auditoría o reprocesamiento.
metadata en cada evento
Si le enviaste metadata a Rekurra — al crear la checkout session
que originó la suscripción, o al crear la suscripción directamente vía
POST /v1/subscriptions — esos mismos pares clave-valor quedan guardados en
la suscripción y se repiten en data.metadata de cada webhook que
dispare esa suscripción durante toda su vida (creación, cancelación, pausa,
reanudación, cambio de plan y cada cobro). Si nunca enviaste metadata,
data.metadata llega como null.
Esto te permite correlacionar un evento con tu propio pedido, usuario o campaña sin tener que ir a buscar la suscripción por separado.
{
"id": "evt_01HZXK7QJ5M8Z9X2R3TVQ7A1B2",
"type": "subscription.created",
"created": "2026-09-17T15:04:05Z",
"data": {
"subscription_id": "sub_01HZXK7QJ5M8Z9X2R3TVQ7A1B2",
"metadata": { "origen": "landing", "order_id": "ORD-4821" }
}
}En los eventos de cobro (charge.approved, charge.declined), data
además incluye charge_attempt_id y status (el estado crudo que devuelve
Wompi):
{
"id": "evt_01HZXK7QJ5M8Z9X2R3TVQ7C4D6",
"type": "charge.approved",
"created": "2026-09-17T15:04:07Z",
"data": {
"subscription_id": "sub_01HZXK7QJ5M8Z9X2R3TVQ7A1B2",
"charge_attempt_id": "ca_01HZXK7QJ5M8Z9X2R3TVQ7E5F7",
"status": "APPROVED",
"metadata": { "origen": "landing", "order_id": "ORD-4821" }
}
}metadata es texto libre que tú envías — Rekurra solo lo persiste y lo
re-serializa, nunca lo interpreta. Tamaño máximo: 60 KB serializado por
objeto (se rechaza al crear la checkout session o la suscripción si lo
superas), así siempre entra dentro del límite de 64 KB del propio payload
del webhook.
Reintentos
Si tu endpoint no responde 200 (o hace timeout), Rekurra reintenta la
entrega con backoff exponencial vía una cola de salida (outbox). Responde
200 tan pronto verifiques la firma — procesa el efecto de forma
asíncrona si es una operación larga.
Ingress: cómo Wompi le habla a Rekurra
Este flujo es interno (no lo implementas tú), pero entenderlo ayuda a saber por qué tus webhooks a veces llegan segundos después del cobro:
POST /webhooks/wompi/{developerId} recibe el evento crudo de Wompi (sin
Bearer token), lo autentica verificando signature.checksum con tu
EventsSecret en tiempo constante, lo persiste (deduplicado por checksum
único) y, en transaction.updated, empareja el ChargeAttempt
correspondiente por wompi_transaction_id o reference. Cuando la
transacción queda APPROVED, Rekurra calcula y descuenta el fee (ver
Saldo prepago) y entonces dispara tu webhook
charge.approved.
Responde: 200 si todo bien, 401 si la firma no es válida, 500 para
forzar un reintento de Wompi, 413 si el cuerpo excede 64 KB.