Eventos de webhook de Stripe: cuáles debe manejar tu app
Qué eventos de webhook de Stripe debe manejar tu app: checkout, suscripción, reembolso y disputa, y cómo manejar duplicados y entregas fuera de orden.

En esta página · 7 secciones
- ¿Qué eventos de webhook de Stripe necesita un checkout simple?
- ¿Qué eventos importan para suscripciones y renovaciones fallidas?
- ¿Y los reembolsos y las disputas?
- ¿Por qué los eventos llegan fuera de orden o más de una vez?
- ¿Cómo hago que mi handler de eventos sea idempotente?
- ¿Qué eventos debería ignorar?
- ¿Quieres una segunda opinión sobre tu handler de webhooks?
Puntos clave
- Empieza con una lista corta de eventos y agrega otros solo cuando una funcionalidad los necesite.
- Otorga acceso a partir de los webhooks, no de la página de éxito a la que se redirige al cliente.
- Stripe puede enviar el mismo evento dos veces y en cualquier orden, así que guarda los IDs de los eventos y haz que los handlers sean idempotentes.
- Las suscripciones necesitan eventos de pago fallido y de cancelación, no solo del primer pago.
- Los reembolsos y las disputas deben actualizar tus propios registros, o tu contabilidad y el control de acceso terminan desalineados.
- Ignora los eventos sobre los que no actúas, pero aun así responde con un 200.
Los eventos de webhook de Stripe que tu app debe manejar dependen de lo que vendes, pero el conjunto esencial es pequeño: checkout.session.completed (o payment_intent.succeeded) para pagos únicos, invoice.paid e invoice.payment_failed para suscripciones, customer.subscription.updated y customer.subscription.deleted para el control de acceso, y charge.refunded más charge.dispute.created para el dinero que se devuelve.
Todo lo demás es opcional. El verdadero trabajo no es suscribirse a muchos eventos, sino manejar bien los pocos que necesitas: verificarlos, tolerar duplicados y entregas fuera de orden, y nunca confiar en el navegador para saber si un pago se completó. Esta es la lógica que los escáneres de seguridad genéricos no detectan, y las apps construidas con IA suelen equivocarse en ella.
¿Qué eventos de webhook de Stripe necesita un checkout simple?
Para una compra única con Stripe Checkout, el evento principal es checkout.session.completed. Te indica que el cliente terminó la sesión. Si usas métodos de pago diferidos, como débitos bancarios, escucha también checkout.session.async_payment_succeeded y checkout.session.async_payment_failed, porque completado no siempre significa pagado. Revisa el campo payment_status de la sesión antes de entregar el pedido.
Si construyes tu propio flujo con Payment Intents, usa payment_intent.succeeded y payment_intent.payment_failed.
- checkout.session.completed: crea el pedido, otorga el acceso, envía el recibo.
- checkout.session.async_payment_succeeded: entrega los pedidos que estaban pendientes.
- checkout.session.async_payment_failed: cancela el pedido pendiente y avísale al usuario.
- checkout.session.expired: libera el inventario reservado, si reservas alguno.
Un error común que veo en el código generado por IA es entregar el pedido en la redirección a la URL de éxito. Un usuario puede llegar a esa URL sin pagar, o cerrar la pestaña antes de llegar. El webhook es la fuente de verdad.
¿Qué eventos importan para suscripciones y renovaciones fallidas?
En las suscripciones vive la mayoría de los bugs, porque el primer pago es solo el comienzo. Las renovaciones ocurren semanas después, sin ningún usuario presente, y tu app solo se entera a través de los eventos.
El conjunto mínimo para suscripciones
- checkout.session.completed: vincula los IDs de cliente y de suscripción de Stripe con tu usuario.
- invoice.paid: el pago funcionó; extiende el acceso por el nuevo período.
- invoice.payment_failed: la renovación falló; notifica al usuario y marca la cuenta como morosa.
- customer.subscription.updated: cambios de plan, cancelación programada, cambios de estado como past_due o unpaid.
- customer.subscription.deleted: la suscripción terminó; retira el acceso.
Decide qué significa una falla
Stripe reintenta los pagos fallidos según la configuración de reintentos de tu dashboard. Tu app necesita una política: ¿el usuario pierde el acceso con la primera falla o cuando se agotan los reintentos? Escríbelo y luego asígnalo al estado de la suscripción. Prefiero derivar el acceso del campo de estado de la suscripción en lugar de un montón de flags actualizados por eventos individuales, porque es más fácil de entender y de reparar.
Considera también customer.subscription.trial_will_end si ofreces períodos de prueba. Se dispara unos días antes de que termine la prueba, un buen momento para enviarle un email al usuario.

¿Y los reembolsos y las disputas?
El dinero puede salir después de la venta, y tu app debería notarlo. Dos grupos de eventos lo cubren.
- charge.refunded: se emitió un reembolso total o parcial, a menudo desde el dashboard por alguien de tu equipo. Actualiza el pedido y decide si revocar el acceso o los créditos.
- charge.dispute.created: un cliente impugnó el cargo ante su banco. Marca la cuenta, reúne evidencia y considera pausar la entrega.
- charge.dispute.closed: ya se conoce el resultado (ganada o perdida). Actualiza tus registros en consecuencia.
Puede que nunca automatices esto por completo, y está bien. Incluso un handler simple que escribe el evento en una tabla y te envía un email es mejor que el silencio. Sin él, un cliente reembolsado conserva un plan pago, o descubres una disputa cuando el plazo de Stripe para presentar evidencia casi se ha vencido. Los plazos de las disputas son reales, así que revisa la documentación actual de Stripe sobre los tiempos.
¿Por qué los eventos llegan fuera de orden o más de una vez?
La documentación de Stripe indica que no garantiza el orden de entrega de los eventos, y que los endpoints pueden recibir el mismo evento más de una vez. Los reintentos ocurren cuando tu servidor excede el tiempo de espera o devuelve un estado distinto de 2xx. Así que las suposiciones que hacen muchos handlers son incorrectas:
- Puedes recibir customer.subscription.updated antes de checkout.session.completed.
- Puedes recibir invoice.paid antes de que tu base de datos sepa que la suscripción existe.
- Puedes recibir el mismo checkout.session.completed dos veces y crear dos pedidos.
Después de trabajar en el equipo de carrito y checkout de Total Wine & More, un minorista estadounidense con 190 millones de visitas al año en su sitio, aprendí a tratar los mensajes duplicados y tardíos como una condición normal, no como un caso extremo. A esa escala, todo lo que puede ocurrir dos veces va a ocurrir. A las apps pequeñas también les pasa, solo que con menos frecuencia, y suele aparecer como un pedido duplicado justo la semana en que llega tu primer tráfico real.
Defensas prácticas
- Responde rápido con un 200 y haz el trabajo pesado en un job en segundo plano, para que Stripe no reintente porque fuiste lento.
- Cuando un evento hace referencia a algo que todavía no tienes, obtén el objeto desde la API de Stripe o falla con un estado que permita reintentar.
- No confíes en el payload como la verdad más reciente. Para decisiones críticas, recupera el objeto actual desde Stripe.

¿Cómo hago que mi handler de eventos sea idempotente?
Idempotente significa que ejecutar el handler dos veces tiene el mismo resultado que ejecutarlo una vez. El patrón confiable más simple usa el ID del evento, que es único para cada evento.
- Verifica primero la firma usando el cuerpo sin procesar de la solicitud y el secret de tu endpoint. Rechaza todo lo que falle.
- Inserta el ID del evento en una tabla con una restricción de unicidad, en la misma transacción de base de datos que tus cambios.
- Si el insert falla porque el ID ya existe, responde con 200 y no hagas nada.
- De lo contrario, aplica el cambio y haz commit.
La restricción de unicidad importa más que un if en el código. Dos entregas que llegan en el mismo instante pueden pasar ambas una verificación de lectura, pero solo una puede ganar el insert.
Agrega una segunda capa en los objetos de negocio: guarda en el pedido el ID de la sesión o del payment intent de Stripe con una restricción de unicidad, para que ni siquiera un bug en el seguimiento de eventos pueda crear dos pedidos para un solo pago. Escribe los handlers como cambios de estado (marcar el estado como pagado) en lugar de incrementos (sumar 30 créditos) siempre que puedas; si tienes que otorgar créditos, vincula la asignación al ID del evento o de la factura.
Si tu app está construida sobre edge functions de Supabase o una configuración similar, aplican las mismas reglas. La base de datos es el lugar correcto para garantizar la unicidad, y la función debe tratarse como algo que puede ejecutarse dos veces. Para un ejemplo real de riesgo a nivel de plataforma, lee sobre la seguridad de Base44 y el hallazgo de Wiz.
¿Qué eventos debería ignorar?
Stripe emite decenas de tipos de eventos. En el dashboard eliges cuáles van a cada endpoint, así que suscríbete solo a los que manejas. Una lista más corta significa menos ruido, menos fallas y menos cosas que probar.
Por lo general innecesarios para una app básica:
- customer.created, customer.updated y la mayoría de los eventos de payment_method, a menos que los sincronices a propósito.
- charge.succeeded y payment_intent.created cuando ya actúas sobre los eventos de checkout o de factura. Manejar ambos lleva a entregas duplicadas.
- invoice.created, invoice.finalized y similares, a menos que personalices las facturas.
- Eventos de productos y precios, si los gestionas solo desde el dashboard.
Si de todos modos llega a tu endpoint un tipo inesperado, regístralo en el log y responde con 200. Devolver errores para eventos desconocidos provoca reintentos y puede llevar a Stripe a desactivar el endpoint tras fallas repetidas.
Una advertencia: ignorar un evento es una decisión, no un accidente. Mantén en tu repositorio una lista corta de los eventos que manejas y por qué, para que el próximo desarrollador, o el próximo prompt de IA, no cambie el comportamiento sin que nadie lo note.
¿Quieres una segunda opinión sobre tu handler de webhooks?
Si quieres que alguien más revise tu configuración de Stripe, escríbeme a me@filipeeduardo.dev con una descripción o el código del handler, y te diré qué revisaría primero.
Preguntas frecuentes
¿Necesito webhooks si uso Stripe Checkout?
Sí. La redirección a tu página de éxito no es prueba de pago. Los webhooks son la forma confiable de saber que un pago se completó, falló o fue reembolsado.
¿Cuál es el evento de webhook de Stripe más importante?
Para ventas únicas es checkout.session.completed, con una verificación del estado del pago. Para suscripciones es invoice.paid, porque impulsa las renovaciones, con invoice.payment_failed muy cerca en segundo lugar.
¿Cómo evito pedidos duplicados por los webhooks?
Guarda cada ID de evento con una restricción de unicidad y procésalo en la misma transacción que tu cambio. Como segunda protección, agrega una restricción de unicidad sobre el ID del pago o de la sesión de Stripe en el pedido.
¿Qué debe responder mi endpoint?
Un estado 2xx en cuanto el evento esté verificado y registrado de forma segura. Devuelve un error solo cuando quieras que Stripe reintente, como ante una falla temporal de la base de datos. Revisa la documentación de Stripe para conocer el comportamiento actual de los reintentos.
¿Un constructor de apps con IA puede configurar esto correctamente?
Puede producir un primer borrador que funcione, pero trátalo como un borrador. Revisa la verificación de firmas, la idempotencia y las reglas de acceso antes de que lleguen pagos reales.

