Migrar a Webhooks Unificados (intent.*)

Recurrente unificó todos los eventos de pago en un solo formato: intent.*. En lugar de un evento distinto por método de pago (payment_intent.*, bank_transfer_intent.*, etc.), recibís un único evento intent.* con una estructura común, discriminada por el campo type. Escribís un handler en vez de uno por tipo.

No hay breaking changes. Recurrente envía ambos formatos: el clásico por tipo y el unificado. Tu integración actual sigue funcionando sin tocar nada; vos elegís a cuáles suscribir tu endpoint y migrás cuando quieras.

Eventos nuevos a configurar

Suscribí tu endpoint a estos eventos desde tu dashboard de Webhooks:

EventoCuándo se emite
intent.succeededCobro exitoso (cualquier método de pago)
intent.pendingCobro iniciado o pendiente (transferencia, cripto)
intent.failedCobro fallido
intent.canceledIntent cancelado
intent.paidSolo para pagos con balance: le llega a la cuenta pagadora

Cómo funciona

  1. Recurrente envía los dos formatos por cada pago: el legacy por tipo (payment_intent.*, etc.) y el unificado (intent.*), como entregas separadas.
  2. En tu dashboard de Webhooks suscribís tu endpoint a los intent.* que te interesen.
  3. Tu handler ramifica según el campo type y procesa.

Si tu endpoint no filtra por tipo de evento, recibirá tanto los payment_intent.* como los intent.* para el mismo pago. Para no procesar el mismo cobro dos veces, suscribite a uno solo de los dos formatos (o chequeá el campo event_type en tu handler).

El payload unificado

1{
2 "type": "payment", // payment | bank_transfer | crypto | balance | cash
3 "event_type": "intent.succeeded",
4 "id": "in_8c3a1f20",
5 "status": "succeeded", // pending | succeeded | failed | canceled | paid
6 "raw_status": "succeeded", // el estado concreto, sin normalizar
7 "amount_in_cents": 25000,
8 "currency": "GTQ",
9 "customer": { "id": "cus_...", "email": "...", "nit": "CF" },
10 "product": { "id": "prod_..." },
11 "checkout": { "...": "..." },
12 "payment": { "...": "..." },
13 "details": { "fee": 875, "installments": 1, "channel": "Tarjeta", "...": "..." }
14}

El envelope y el núcleo común son idénticos para los cinco tipos; solo cambia type y el contenido de details. Ver la estructura completa en la documentación de Webhooks.

Mapeo: legacy → unificado

Evento legacyEvento unificadotype
payment_intent.succeededintent.succeededpayment
payment_intent.failedintent.failedpayment
payment_intent.requires_capture / requires_verificationintent.pendingpayment
bank_transfer_intent.{pending,succeeded,failed}intent.{pending,succeeded,failed}bank_transfer
crypto_intent.{pending,succeeded,failed}intent.{pending,succeeded,failed}crypto
balance_intent.succeededintent.succeededbalance
balance_intent.paidintent.paidbalance
cash_intent.{succeeded,failed,canceled}intent.{succeeded,failed,canceled}cash

Los estados intermedios de tarjeta (requires_capture, requires_verification) se normalizan a pending. El estado concreto siempre está disponible en raw_status.

Pasos para migrar

  1. Revisá el payload nuevo. Mirá la estructura en la documentación de Webhooks, o consultá un pago real con GET /api/intents/{id} (devuelve el mismo formato que el webhook).
  2. Actualizá tu handler para consumir intent.* y ramificar por type:
    1switch (event.type) {
    2 case "payment": /* event.details.fee, .installments, ... */ break
    3 case "bank_transfer":
    4 case "crypto":
    5 case "balance":
    6 case "cash": break
    7}
    8// el estado siempre en event.status: pending | succeeded | failed | canceled | paid
  3. Suscribí tu endpoint a los intent.* desde el dashboard de Webhooks.
  4. (Opcional) Dá de baja los eventos por tipo. Cuando tu handler unificado funcione, desuscribite de los payment_intent.*, bank_transfer_intent.*, etc., para dejar de recibir el formato duplicado.

Eventos que no cambian

subscription.*, refund.*, dispute.* y setup_intent.* siguen exactamente igual — no son parte del recurso Intent unificado.