Suscripciones con Stripe en tu app: cómo hacerlo bien
Cómo montar suscripciones con Stripe que no se desincronizan: qué guardar, qué webhooks escuchar, cómo validar el acceso en el servidor y cómo probarlo.
Read in English →
La forma confiable de cobrar suscripciones con Stripe es dejar que Stripe sea el dueño del estado de cobro y que tu app lo copie a través de webhooks. Mandas al cliente a una sesión de Checkout en modo suscripción, guardas el ID de cliente y el de suscripción de Stripe junto a tu usuario, y actualizas una tabla pequeña cada vez que Stripe envía un evento de suscripción o de factura. Luego tu app decide quién tiene acceso leyendo esa tabla en el servidor, nunca confiando en la página de "pago exitoso".
En esta guía verás por qué esa página no sirve como señal, qué datos guardar, qué eventos atender, cómo validar el acceso y cómo probar una renovación y un pago fallido antes de que lo descubran tus clientes.
¿Por qué la página de éxito no debe activar el plan pagado?
Es lo primero que hacen casi todas las apps hechas con IA: cuando Checkout redirige a /success, la página escribe en la base de datos "este usuario es pro". En una demo funciona; en producción falla por tres motivos.
- El cliente puede cerrar la pestaña antes de la redirección. Pagó, pero tu app nunca se enteró.
- Cualquiera puede abrir
/successa mano. Si esa página da acceso, es un botón de upgrade gratis. - El primer cobro es solo el comienzo. Renovaciones, tarjetas rechazadas, cambios de plan y cancelaciones pasan después, sin ningún navegador de por medio. Solo los webhooks te avisan.
La propia documentación de Stripe dice que una integración de suscripciones necesita un destino de webhooks, porque la mayor parte de la actividad ocurre de forma asíncrona. Usa la página de éxito como un "gracias, estamos activando tu cuenta" y deja el trabajo real al webhook.
¿Qué datos conviene guardar en tu base de datos?
Una tabla pequeña que refleje lo que hay en Stripe. No hace falta copiarlo todo.
| Columna | De dónde sale | Para qué sirve |
|---|---|---|
user_id | Tu sistema de login | Une la fila con una persona |
stripe_customer_id | El cliente que creas antes de Checkout | Encontrar al usuario cuando llega un evento |
stripe_subscription_id | Eventos customer.subscription.* | Una fila por suscripción |
status | Objeto de suscripción | active, trialing, past_due, canceled, unpaid, etc. |
price_id o lookup key | Primer ítem de la suscripción | Saber en qué plan está |
current_period_end | Ítem de la suscripción | Hasta cuándo está pagado |
cancel_at_period_end / cancel_at | Objeto de suscripción | Mostrar "tu plan termina el…" |
Hay dos detalles que confunden. En las versiones recientes de la API, las fechas del período de cobro están en los ítems de la suscripción y no en la suscripción misma, así que el código copiado de un tutorial viejo puede leer un campo vacío. Y en las suscripciones con el modo de cobro flexible, Stripe indica una cancelación programada con cancel_at; en el modo clásico se usa cancel_at_period_end.
Crea tú el cliente de Stripe antes de abrir Checkout, guarda su ID en el usuario y pásalo a la sesión. Buscar al usuario por correo después es frágil: mucha gente paga con un correo distinto al de su cuenta.
¿Qué eventos de webhook necesitas de verdad?
Puedes suscribirte a decenas de eventos. Para un SaaS típico bastan estos:
checkout.session.completed: el cliente terminó Checkout. Toma el ID de suscripción de la sesión y crea la fila.customer.subscription.created,customer.subscription.updated,customer.subscription.deleted: copia estado, plan, fin de período y campos de cancelación en cada cambio.deletedsignifica que la suscripción terminó.invoice.paid: se pagó una renovación. Stripe recomienda consultar la suscripción y confirmar que estéactiveantes de extender el acceso.invoice.payment_failed: falló un cobro. Avisa al cliente; no cortes el acceso todavía si tu configuración de reintentos va a volver a intentarlo.customer.subscription.trial_will_end: llega 3 días antes de que termine la prueba gratis. Buen momento para comprobar que hay un método de pago guardado.
Si también quieres reaccionar a reembolsos, disputas y alertas de fraude, agrega charge.refunded, charge.dispute.created y radar.early_fraud_warning.created. Lo explicamos en la guía de reembolsos y disputas en Stripe.
¿Cómo se escribe el manejador?
El patrón más robusto es: "ante cualquier evento de suscripción, pido la suscripción a Stripe y sobrescribo mi fila". Así el orden deja de importar. Stripe no garantiza que los eventos lleguen en orden y puede entregar el mismo evento dos veces; si consultas el objeto actual, un evento atrasado no puede pisar datos nuevos con datos viejos.
Tres reglas más:
- Verifica la firma con el cuerpo crudo de la petición antes de hacer nada. Si ese paso falla, revisa el error de firma del webhook de Stripe.
- Guarda los IDs de eventos procesados y salta los repetidos.
- Responde con un 2xx rápido y deja el trabajo lento para después; si no, Stripe da la entrega por fallida y la reintenta.
¿Cómo validas el acceso en el servidor?
Cada petición que toca una función de pago debe hacerle una sola pregunta a tu backend: ¿este usuario tiene una fila con un estado permitido? Una versión mínima:
select exists (
select 1 from subscriptions
where user_id = auth.uid()
and status in ('active', 'trialing')
) as tiene_acceso;
Decide a propósito qué significa past_due para ti. Muchos SaaS mantienen el acceso mientras Stripe reintenta el cobro y muestran un aviso para actualizar la tarjeta; otros lo restringen de inmediato. La documentación de Stripe sugiere quitar el acceso cuando el estado pasa a canceled o unpaid. Lo desarrollamos en cómo limitar funciones por plan en el servidor, incluido por qué ocultar un botón en React no es un muro de pago.
¿Cómo pruebas las renovaciones antes de salir a producción?
Esperar un mes a que llegue la renovación no es un plan de pruebas. En un sandbox tienes dos herramientas:
- Relojes de prueba (test clocks). La documentación de Stripe ahora los agrupa bajo el nombre Simulations. Asocias un cliente de prueba a un reloj, lo suscribes y adelantas el tiempo más allá de la fecha de renovación para ver cómo responden la factura, el cobro y tu webhook.
- El Stripe CLI. Con
stripe listen --forward-to localhost:3000/api/stripe/webhookrecibes los eventos en tu computadora, y constripe triggerdisparas eventos concretos cuando quieras.
Prueba al menos estos caminos: primer pago exitoso, renovación, renovación con una tarjeta que se rechaza, subida de plan y cancelación al final del período. Las tarjetas de prueba de Stripe incluyen números que se rechazan, así ves aparecer past_due sin tocar una tarjeta real.
Respuestas rápidas
¿Uso Checkout o armo mi propio formulario para suscripciones? Para la mayoría de los SaaS pequeños, Checkout: resuelve pruebas gratis, impuestos, descuentos y métodos de pago. En Stripe Checkout o Payment Element vemos cuándo vale la pena un formulario propio.
¿Basta con checkout.session.completed? No. Solo te informa de la primera compra. Las renovaciones, los fallos y las cancelaciones llegan por eventos de suscripción y de factura.
¿Puedo guardar el plan en los metadatos del usuario en vez de una tabla? Se puede, pero una tabla con estado y fin de período es más fácil de consultar, auditar y corregir cuando algo sale mal.
¿Qué pasa si mi webhook estuvo caído un día? En modo live, Stripe reintenta las entregas fallidas durante un máximo de tres días. También puedes listar los eventos recientes con la API y reprocesar los que se perdieron.
¿Quieres que alguien revise tus suscripciones?
Si Stripe cobra pero el estado del plan en tu app se desincroniza, la causa casi siempre está en el manejador del webhook o en la validación de acceso. De eso se trata nuestro servicio de pagos con Stripe. Puedes empezar con la revisión gratuita de tu app, leer la documentación de webhooks para suscripciones o contarnos qué pasa; una persona te responde en un día hábil.


