Alertas de fallos de pago en Stripe: disputas, rechazos y webhooks muertos

Stripe te avisa por email de disputas y pagos fallidos. Así conviertes los eventos de webhook de Stripe en un push, un aviso urgente o una llamada.

Actualizado

Índice

Stripe ya sabe cuándo se abre una disputa, cuándo falla el cobro de una suscripción o cuándo rebota una transferencia. Te lo cuenta por email. Para conseguir algo más ruidoso, registra en Stripe un segundo endpoint de webhook que apunte a la URL de un canal de Echobell, suscríbelo a un puñado de tipos de evento y elige el tipo de notificación según el plazo que lleva pegado cada uno. Las disputas y los avisos tempranos de fraude tienen un reloj en marcha; una renovación fallida normalmente no.

Esta guía cubre qué eventos de Stripe merecen interrumpirte, cómo apuntar Stripe a Echobell en unos cinco minutos, los campos de plantilla que realmente existen en cada objeto y el compromiso que aceptas al saltarte la verificación de firma.

Los eventos que de verdad merecen una interrupción

Las alertas de pagos fallan siempre igual: alguien se suscribe a payment_intent.succeeded porque sienta bien, el móvil vibra cuarenta veces al día y seis semanas después una notificación de disputa pasa de largo sin leerse. Empieza por el plazo. Si perderte el evento durante ocho horas no cuesta nada, no necesita llegarte en ocho segundos.

EventoPor qué importaTipo sugerido
charge.dispute.createdTienes una ventana limitada para responder — normalmente de 7 a 21 días según la red de tarjetas. Si la pierdes, pierdes automáticamente.Llamada
radar.early_fraud_warning.createdEl emisor de la tarjeta ha avisado a Stripe de que un cargo puede ser fraudulento. Reembolsar antes de que se convierta en disputa formal es la acción que aún te queda, y la ventana es corta.Llamada
payout.failedEl dinero que Stripe recaudó no llega a tu banco. Todo lo que viene después — nóminas, cálculo de runway — está ahora mal.Llamada
invoice.payment_failedChurn involuntario. Se resuelve solo lo bastante a menudo como para que una llamada sea excesiva, pero las cuentas más grandes merecen una mirada el mismo día.Urgente
customer.subscription.deletedChurn voluntario. Merece saberse hoy, no merece despertarte.Normal
payment_intent.succeededNo hay nada roto. Este es el evento que te entrena para ignorar los otros cinco.Nada

Los niveles se corresponden con los tres tipos de notificación de Echobell: Normal es un push corriente, Urgente atraviesa la mayoría de modos de concentración y Llamada se presenta como una llamada entrante, así que suena incluso con No molestar. Cada suscriptor elige su propio nivel por canal, de modo que una cofundadora puede recibir las disputas como llamada mientras un compañero de soporte las recibe como push.

Lo que necesitas

  • Una cuenta de Stripe con acceso a la pestaña Webhooks de Workbench
  • Echobell instalado (App Store / Google Play)
  • Cinco minutos. Sin servidor, sin despliegue, sin código — salvo que quieras verificación de firma, que está en la última sección.

Paso 1 — Un canal por tipo de evento

Es tentador crear un único canal «Stripe» y mandarlo todo ahí. No lo hagas. La plantilla del cuerpo y el enlace al dashboard son distintos para una disputa, una factura y una transferencia, porque cada una transporta un objeto diferente — y el sentido de todo este montaje es que la notificación te diga qué ha pasado sin abrir nada.

Crea un canal con el nombre del evento: Stripe Disputes. Escribe las plantillas de título y cuerpo para que se lean de un vistazo en la pantalla de bloqueo:

Title: 🔴 Dispute opened — {{data.object.reason}}
Body: Amount: {{data.object.amount}} {{data.object.currency}}
Charge: {{data.object.charge}}
Status: {{data.object.status}}

Configura la plantilla de enlace en los ajustes avanzados para que el registro de la notificación abra la página correcta:

https://dashboard.stripe.com/disputes/{{data.object.id}}

Después suscríbete con el tipo Llamada y copia la URL del webhook desde la vista de detalle del canal. Tiene esta forma: https://hook.echobell.one/t/<channel-token>.

Activa POST Only en los ajustes avanzados del canal. Stripe siempre envía POST, y ese interruptor hace que pegar la URL en un chat o en una pestaña del navegador ya no pueda disparar una alerta de disputa falsa.

Paso 2 — Apunta Stripe al canal

En el dashboard de Stripe, abre la pestaña Webhooks y crea un destino de eventos:

Pulsa Create an event destination, selecciona Your account y deja la versión de la API en el valor por defecto de tu cuenta.

Selecciona exactamente un tipo de evento — charge.dispute.created para este canal. El propio consejo de Stripe es suscribirse solo a los eventos que tu integración necesita; aquí además mantiene honesta la plantilla, porque todos los payloads que llegan tienen la misma forma.

Elige Webhook endpoint como tipo de destino y pega la URL del canal de Echobell.

Guarda y usa Send test event — o stripe trigger charge.dispute.created desde la CLI — para confirmar que el teléfono suena.

Repite para cada canal que hayas creado. Stripe permite hasta 16 endpoints de webhook por cuenta, de sobra para uno por nivel de alerta.

Paso 3 — Qué llega en realidad

Stripe envía el objeto Event como JSON. Echobell lee el cuerpo tal cual, así que cada campo es accesible en plantillas y condiciones con notación de punto:

{
  "id": "evt_1P...",
  "type": "charge.dispute.created",
  "livemode": true,
  "created": 1757548800,
  "data": {
    "object": {
      "id": "dp_1P...",
      "amount": 4900,
      "currency": "usd",
      "reason": "fraudulent",
      "status": "needs_response",
      "charge": "ch_3P...",
      "evidence_details": { "due_by": 1759449600 }
    }
  }
}

Tres cosas de este payload sorprenden a casi todo el mundo:

Los importes son enteros en la unidad monetaria mínima. Un amount de 4900 son 49,00 $. Las plantillas de Echobell interpolan y comparan valores, pero no hacen aritmética, así que {{data.object.amount}} muestra 4900. O lo etiquetas con honestidad (Amount: 4900 (cents)) o usas el reenviador de la última sección para dividir entre 100 antes de enviar.

Las marcas de tiempo son segundos Unix. {{data.object.evidence_details.due_by}} se muestra como 1759449600, no como una fecha. Si lo que importa es que hay un plazo más que la hora exacta, quítalo de la plantilla — la página de la disputa lo muestra — y deja que trabaje la plantilla de enlace.

Los nombres de campo cambian según el objeto. Una disputa tiene amount; una factura tiene amount_due, customer_email, attempt_count y hosted_invoice_url; una transferencia tiene failure_message y arrival_date; un aviso temprano de fraude tiene fraud_type, actionable y charge como ID de texto plano. Una variable inexistente se renderiza como cadena vacía en lugar de dar error, así que una plantilla copiada del canal equivocado falla en silencio. Esta es la razón práctica de un canal por tipo de evento.

Paso 4 — Filtra con condiciones, no con fuerza de voluntad

Las condiciones de canal usan la misma sintaxis de expresiones que las plantillas, sin las llaves, y se evalúan antes de entregar nada.

La que debería llevar todo canal de Stripe:

livemode == true

El tráfico en modo de prueba — tus propios stripe trigger, un compañero trasteando en un sandbox — deja de llegarte al móvil. Añádela después de confirmar que el cableado funciona, no antes.

Para el canal de facturas fallidas, un umbral mantiene las cuentas pequeñas fuera de tus tardes:

livemode == true && data.object.amount_due > 20000

Se lee como «más de 200 $», en céntimos. Y si prefieres ver los reintentos que están realmente atascados en lugar de cada primer intento:

livemode == true && data.object.attempt_count > 1

Si al final sí apuntaste un endpoint con varios tipos de evento a un solo canal, las condiciones los vuelven a separar:

type == "charge.dispute.created" || type == "payout.failed"

Paso 5 — Mantén la vía de aviso fuera de lo que se rompe

Esta parte vale más que las plantillas.

Tu endpoint de webhook de producción es donde ocurre el cumplimiento: concede accesos, escribe en la base de datos, envía el recibo. Es, por tanto, el endpoint que se cae cuando se cae tu aplicación. Cuando eso pasa, Stripe reintenta hasta tres días con backoff exponencial y te manda un email — y un email sobre webhooks no entregados es idéntico a cualquier otro email de Stripe, que es justo por lo que se encuentra el lunes.

La muerte silenciosa de un webhook tiene causas aburridas. Stripe trata una redirección 3xx como fallo, así que un endpoint que empieza a redirigir http a https o a añadir una barra final deja de recibir eventos. Exige TLS 1.2 o superior, así que basta con un certificado caducado o mal configurado. Un 403 de una regla de WAF que alguien añadió la semana pasada también sirve.

Un segundo endpoint apuntado directamente a Echobell no comparte nada de eso. Es otra URL, en otro host, con otro certificado, y sigue sonando mientras tu aplicación está caída. La regla se generaliza: el camino que te avisa de que algo está roto no debería pasar por aquello que está roto.

Aun así quieres que el fallo de tu propio endpoint sea visible. Revisa la pestaña Event deliveries en Workbench cuando algo huela raro — muestra Delivered, Pending y Failed por evento, con el estado HTTP de cada intento. Stripe permite reenviar un evento hasta 15 días desde el dashboard, o 30 con stripe events resend en la CLI, así que un hueco detectado en quince días es recuperable.

La URL del canal de Echobell es una credencial de portador: quien la tenga puede disparar el canal. Apuntar Stripe directamente ahí significa que nadie verifica la cabecera Stripe-Signature, así que una URL filtrada es una máquina de alertas falsas, no una brecha de datos. Mantenla fuera de repositorios y capturas, usa Reset Token si se escapa, y lee la siguiente sección si el compromiso te incomoda.

Opcional — Verifica la firma antes

Si quieres que la firma de Stripe se verifique de verdad y que los importes se vean como dinero, pon delante un pequeño reenviador. Este Cloudflare Worker verifica el evento, devuelve 200 de inmediato como pide Stripe y envía a Echobell un payload plano:

import Stripe from "stripe";

export default {
  async fetch(request, env, ctx) {
    const stripe = new Stripe(env.STRIPE_SECRET_KEY);
    const body = await request.text();

    let event;
    try {
      event = await stripe.webhooks.constructEventAsync(
        body,
        request.headers.get("stripe-signature"),
        env.STRIPE_WEBHOOK_SECRET,
      );
    } catch {
      return new Response("invalid signature", { status: 400 });
    }

    const invoice = event.data.object;
    ctx.waitUntil(
      fetch(env.ECHOBELL_HOOK_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          customer: invoice.customer_email || invoice.customer,
          amount: (invoice.amount_due / 100).toFixed(2),
          currency: invoice.currency.toUpperCase(),
          attempt: invoice.attempt_count,
          externalLink: invoice.hosted_invoice_url,
        }),
      }),
    );

    return new Response("ok", { status: 200 });
  },
};

La plantilla del otro lado queda mucho mejor, porque el moldeado ya ocurrió en código:

Title: 💳 Payment failed — {{currency}} {{amount}}
Body: Customer: {{customer}}
Attempt #{{attempt}}

externalLink es una variable especial: sin plantilla de enlace configurada, Echobell la usa como enlace del registro de notificación, así que la factura alojada queda a un toque.

Fíjate en la forma del compromiso: esto es ya una pieza de infraestructura que puede fallar por su cuenta, exactamente lo que advierte el paso 5. Un término medio razonable es verificar firmas en el canal de mucho volumen, donde las alertas falsas molestarían, y dejar el canal de disputas cableado directamente, donde el coste de un timbrazo espurio es una mirada de desconcierto y el de uno perdido es el importe disputado.

Lo que este montaje no te da

  • Ni turnos de guardia ni escalado. Todos los suscritos a un canal de llamada suenan a la vez. Es una virtud con cuatro personas y un problema con cuarenta; con cuarenta quieres una plataforma de incidentes.
  • Sin deduplicación. Stripe no garantiza el orden de los eventos y puede entregar el mismo más de una vez. Dos timbrazos por una disputa son posibles.
  • Sin acuse de recibo. Nada registra que un humano lo vio, y nada escala a una segunda persona si nadie responde.
  • Sin alerta de «los pagos se han parado». Stripe emite eventos cuando algo pasa, nunca cuando algo deja de pasar. Si tu checkout se rompe, no se dispara ningún evento. Eso requiere una tarea programada por tu parte que haga ping a un canal cuando el número de cobros de la última hora sea cero — un interruptor de hombre muerto basado en cron.

Resolución de problemas

El evento de prueba muestra 200 en Stripe pero no llegó ninguna notificación. Echobell responde 200 con un cuerpo JSON incluso cuando no entrega — mira el cuerpo de la respuesta en la pestaña Event deliveries. success: false con un token de longitud válida significa que el token del canal es incorrecto. Si success es true, lo más probable es una condición: livemode == true bloquea todos los eventos de prueba por diseño.

Stripe informa de 405 Method Not Allowed. El canal tiene POST Only activado y algo envió un GET. Stripe siempre hace POST, así que fue una vista previa de enlace o una pestaña del navegador, no Stripe.

La notificación llega con campos en blanco. La plantilla apunta al objeto equivocado — {{data.object.amount}} en un canal de facturas, donde el campo es amount_due. Envía un evento real, ábrelo en el dashboard y lee el JSON.

Las entregas empiezan a fallar tras semanas funcionando. Revisa el certificado y cualquier redirección delante de la URL. Con la URL del canal usada directamente es raro; con un reenviador desplegado por ti, es el sospechoso habitual.

Preguntas frecuentes

¿Puede Stripe llamarme al móvil cuando se abre una disputa?

Por sí solo no. Stripe avisa por email, en el dashboard, mediante el evento charge.dispute.created y por push si usas la app Stripe Dashboard. Para que suene de verdad, enruta ese evento a un canal cuyo tipo de suscripción sea Llamada.

¿Necesito escribir código para conectar Stripe con Echobell?

No. Stripe envía JSON a cualquier URL HTTPS pública, y la URL de un canal de Echobell lo es. Solo hace falta código si quieres verificar la cabecera Stripe-Signature o reformatear los importes.

¿Es seguro dar a Stripe una URL de webhook de terceros?

Es un compromiso deliberado. El payload que envía Stripe contiene metadatos del cliente y del pago, y Echobell no almacena de forma permanente los payloads crudos — la notificación renderizada vive en tu dispositivo. Lo que cedes es la verificación de firma: quien conozca la URL puede enviarte una falsificación convincente. Trátala como una clave de API y usa el patrón del reenviador para lo que prefieras verificar.

¿Por qué mi alerta muestra 4900 en vez de 49,00 $?

Stripe envía los importes como enteros en la unidad monetaria mínima, y las plantillas de Echobell no hacen aritmética. Etiqueta la unidad en la plantilla o divide entre 100 en un reenviador antes de enviar.

¿Cómo evito que los eventos de modo de prueba me despierten?

Añade la condición livemode == true al canal. Stripe marca como livemode: false todos los eventos de sandbox y de stripe trigger.

¿Puede mi cofundador recibir las mismas alertas sin pagar un asiento?

Sí. Comparte el enlace del canal; cada suscriptor elige su tipo de notificación. Una persona puede recibir las disputas como llamada y otra como push normal, y no hay precio por asiento para los suscriptores.

¿Debería alertar sobre pagos exitosos?

Solo un tiempo, y solo mientras el negocio sea lo bastante pequeño como para que cada uno siga siendo un acontecimiento. En cuanto la notificación de pago exitoso se vuelve rutina, empieza a erosionar tu respuesta a las que importan — el mecanismo central de la fatiga por alertas.

Para terminar

Todo el montaje es un destino de eventos de Stripe por canal de Echobell, una condición livemode == true y la disciplina de reservar el nivel de llamada para los eventos que llevan un reloj. Las disputas y los avisos tempranos de fraude lo llevan. Una renovación fallida de un plan de 9 $ no, y fingir lo contrario es justo como acabas durmiendo durante la que sí lo llevaba.

Descarga Echobell para iPhone o consíguelo en Google Play, crea primero el canal de disputas y lanza un stripe trigger charge.dispute.created antes de confiarle a esta vía nada importante.


Relacionado