Saltar al contenido
← Volver al blog
integracionesapps

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.

Por Equipo Devonauts6 min de lectura
Read in English →
Unas manos escriben en una laptop mientras una sostiene una tarjeta bancaria verde sobre un escritorio de madera

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 /success a 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.

ColumnaDe dónde salePara qué sirve
user_idTu sistema de loginUne la fila con una persona
stripe_customer_idEl cliente que creas antes de CheckoutEncontrar al usuario cuando llega un evento
stripe_subscription_idEventos customer.subscription.*Una fila por suscripción
statusObjeto de suscripciónactive, trialing, past_due, canceled, unpaid, etc.
price_id o lookup keyPrimer ítem de la suscripciónSaber en qué plan está
current_period_endÍtem de la suscripciónHasta cuándo está pagado
cancel_at_period_end / cancel_atObjeto de suscripciónMostrar "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:

  1. checkout.session.completed: el cliente terminó Checkout. Toma el ID de suscripción de la sesión y crea la fila.
  2. customer.subscription.created, customer.subscription.updated, customer.subscription.deleted: copia estado, plan, fin de período y campos de cancelación en cada cambio. deleted significa que la suscripción terminó.
  3. invoice.paid: se pagó una renovación. Stripe recomienda consultar la suscripción y confirmar que esté active antes de extender el acceso.
  4. 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.
  5. 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:

  1. 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.
  2. El Stripe CLI. Con stripe listen --forward-to localhost:3000/api/stripe/webhook recibes los eventos en tu computadora, y con stripe trigger disparas 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.

¿Tu app sigue trabada?

Pasa el chequeo gratis con la URL de tu app, o pide la Auditoría de rescate de $299: un ingeniero senior revisa el código y te envía un informe escrito en 2 días hábiles.