Facturación con Stripe para SaaS: las partes que los docs omiten
Prorrateo, pagos fallidos, migraciones de plan y los webhooks que de verdad necesitas. Los casos límite de facturación que muerden en el mes tres.
El camino feliz de Stripe es un placer: pones Checkout, cobras una tarjeta, creas una suscripción. Tendrás la facturación "funcionando" en una tarde. Luego llega el mes tres — una tarjeta vence, alguien sube de plan a mitad de ciclo, un cliente se va y vuelve — y descubres que el camino feliz era como el 30% del trabajo. Este es el otro 70%.
Tu base de datos es la fuente de verdad, no la API de Stripe
El error más común que veo es tratar a Stripe como el lugar del que lees el estado de la suscripción en cada petición. No lo hagas. Stripe es el sistema de registro de los pagos; tu base de datos es el sistema de registro del acceso. Refleja el estado de la suscripción localmente y actualízalo desde los webhooks. Así, "¿este usuario puede hacer X?" es una lectura local rápida, no una llamada de red que puede limitarse o expirar en el peor momento.
Los webhooks son la integración de verdad
Checkout es la puerta de entrada; los webhooks son la tubería que mantiene honesto tu estado local después de que el cliente se va. Estos son los eventos que importan, y los que el quickstart nunca te hace manejar:
// stripe/webhook.ts — the events that actually keep billing honest.
export async function handleStripeEvent(event: Stripe.Event) {
switch (event.type) {
case "invoice.payment_failed":
// Don't cut access yet — Stripe will retry. Flag and notify.
await markPastDue(event.data.object.customer);
break;
case "customer.subscription.deleted":
await revokeAccess(event.data.object.customer);
break;
case "customer.subscription.updated":
// Plan changes, proration, cancel-at-period-end all land here.
await syncSubscription(event.data.object);
break;
}
}subscription.deleted procesado dos veces que revoca dos veces es inofensivo; un crédito otorgado dos veces es un reembolso esperando a pasar.Los casos límite que de verdad muerden
- Un pago fallido no es churn. Un
payment_failedes el inicio de una secuencia de reintentos, no el fin de una suscripción. Marca la cuenta como en mora, escribe al cliente y deja correr el dunning de Stripe antes de cortar el acceso. Matar el acceso al primer fallo enfurece a buenos clientes cuya tarjeta solo venció. - El prorrateo es una función, no un fallo. Cuando alguien sube de plan a mitad de ciclo, Stripe prorratea por defecto y la siguiente factura se ve rara para quien no lo espera. Decide tu política — prorratear, o cobrar completo en el siguiente ciclo — y muéstrala en la UI antes de que un cargo los sorprenda.
- Las migraciones de plan necesitan un plan. Cuando cambias precios, los clientes existentes no deberían saltar de nivel en silencio. Mantenlos en su plan explícitamente, o migra en una fecha que comuniques. Los price IDs son para siempre; trata cambiar uno como una migración, no una edición.
- Cancelar ≠ eliminar. Cancelar al fin del periodo mantiene el acceso hasta que termina el periodo pagado. No revoques en el evento de cancelación — revoca cuando la suscripción de verdad termina. Dos webhooks distintos, dos momentos distintos.
Reconcilia, no confíes
Los webhooks se pierden. Las redes fallan. En algún momento tu estado local y el de Stripe no coincidirán, y no lo notarás hasta que un cliente escriba. Corre un job nocturno de reconciliación que trae las suscripciones activas de Stripe y las compara con tu base de datos. Son cincuenta líneas que convierten la "deriva silenciosa de facturación" en "una línea de log sobre la que puedes actuar".
La facturación no es una función que construyes una vez. Es un sistema que operas — y los docs solo cubren la construcción.