GuíasWebhooks

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
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"]
  }'
201 Created
{
  "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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Donde 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.created
  • subscription.canceled
  • subscription.paused
  • subscription.resumed
  • subscription.past_due
  • subscription.plan_changed
  • charge.approved
  • charge.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.

Payload de ejemplo — subscription.created
{
  "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):

Payload de ejemplo — charge.approved
{
  "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.