← Volver a artículos

3 de junio de 2026

10 min de lectura

Suscripciones con Stripe: modelo de datos, eventos y casos de borde en proyectos SaaS

Cómo modelar el ciclo de vida completo de una suscripción con Stripe Billing: desde el trial hasta la cancelación y los reintentos de cobro.

Read in English

El modelo de datos de Stripe Billing

Stripe Billing trabaja con cuatro objetos principales: Customer, Product, Price y Subscription. El Customer representa al usuario o empresa que paga. El Product describe qué se vende. El Price define el monto, la moneda y el intervalo de cobro. La Subscription conecta un Customer con uno o más Prices y gestiona el ciclo de vida del cobro.

Un error común es sincronizar solo el estado activo de la suscripción y asumir que todo lo demás es irrelevante. En la práctica, el sistema necesita reaccionar a transiciones de estado, a intentos de cobro fallidos, a periodos de gracia y a actualizaciones de plan. Todo eso llega por webhooks, no por polling.

Los eventos que no se pueden ignorar

Stripe emite decenas de tipos de eventos, pero para un SaaS con suscripciones hay cinco que son críticos: `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_succeeded` e `invoice.payment_failed`. Cualquier otro evento puede ignorarse inicialmente; estos cinco no.

El evento `customer.subscription.updated` es especialmente importante porque cubre cambios de plan, cancelaciones programadas, reactivaciones y actualizaciones de periodo de prueba. Su payload incluye el estado anterior y el nuevo, lo que permite implementar lógica diferencial sin consultar la API de Stripe.

Manejo de pago fallido: actualizar estado y notificar al usuario.

case 'invoice.payment_failed': {
  const invoice = event.data.object as Stripe.Invoice;
  const subscriptionId = invoice.subscription as string;

  await db.subscriptions.update({
    where: { stripeSubscriptionId: subscriptionId },
    data: {
      status: 'past_due',
      lastPaymentError: invoice.last_finalization_error?.message ?? null,
    },
  });

  await notifications.send({
    type: 'payment_failed',
    userId: invoice.customer as string,
    retryAt: invoice.next_payment_attempt
      ? new Date(invoice.next_payment_attempt * 1000)
      : null,
  });

  break;
}

Casos de borde en el ciclo de vida de una suscripción

El trial ending es uno de los casos más frecuentes: Stripe envía `customer.subscription.trial_will_end` tres días antes del fin del periodo de prueba. Ese evento es la oportunidad correcta para recordarle al usuario que actualice su método de pago antes de que se intente el primer cobro real.

Los upgrades y downgrades de plan tienen su propia lógica de prorratas. Por defecto, Stripe genera un crédito o cargo inmediato al cambiar de Price. Si el comportamiento esperado es diferente (por ejemplo, cambiar el plan al próximo ciclo sin prorratear), hay que configurar `proration_behavior` explícitamente al crear la suscripción o al actualizarla.

  • Escucha `trial_will_end` para recordar el método de pago con anticipación.
  • Maneja `subscription.updated` con lógica diferencial sobre el campo `status`.
  • Configura `cancel_at_period_end` si quieres permitir cancelación al final del ciclo.
  • Prueba el flujo de dunning con las tarjetas de prueba designadas de Stripe.

Sincronizar el estado de Stripe con tu base de datos

La base de datos del producto debe ser la fuente de verdad para el acceso y los features. Stripe es la fuente de verdad para el estado de pago. Esas dos cosas tienen que estar sincronizadas, pero no son lo mismo: un usuario puede tener acceso activo aunque Stripe esté en periodo de gracia, dependiendo de la política del negocio.

El patrón más limpio es guardar el `stripeSubscriptionId`, el `stripeCustomerId`, el `status` de Stripe y la `currentPeriodEnd` en la tabla de suscripciones del producto. Al recibir cualquier webhook relevante, se actualizan esos campos y se recalcula el acceso. Nunca se consulta la API de Stripe en tiempo de request para verificar si un usuario tiene acceso.

Más artículos

Volver a artículos