@dariomvg/create
stripesuscripcionespagos-recurrenteswebhooksintegracion

Stripe Suscripciones — Configuración

Módulo autocontenido para pagos recurrentes: crear, leer, cancelar, pausar, reanudar, cambiar de plan, cambiar la cantidad, trials, estado, renovación y expiración.

Está separado de lib/stripe/ (pagos únicos) y lib/stripe-billing/ (portal del cliente, facturas, métodos de pago): no comparten código ni endpoint de webhook. Los tres envuelven la misma cuenta de Stripe pero mantienen sus responsabilidades independientes.

Supuesto: este módulo opera sobre un priceId existente (creado en el Stripe Dashboard, o por lo que sea que gestione tu catálogo de productos). No crea Products ni Prices.

Dependencias

  • stripe — SDK del lado del servidor, usado en lib/stripe-subscriptions/service.ts
  • @stripe/stripe-js — SDK del lado del cliente, usado en lib/stripe-subscriptions/client.ts

No se instalan automáticamente. Si ya los instalaste para los otros módulos, estás cubierto: son los mismos paquetes.

Mapa de archivos

lib/stripe-subscriptions/
├── service.ts   # Internal logic + Stripe SDK instance. Don't touch/import directly elsewhere.
├── actions.ts   # Server Actions — import into Server Components.
├── types.ts     # Shared types — safe to import anywhere.
└── client.ts    # Client-side helpers — import into Client Components.

app/api/webhooks/stripe-subscriptions/
└── route.ts     # Subscriptions webhook endpoint. Register this URL in your Stripe Dashboard.

.env.local       # Adds STRIPE_SUBSCRIPTIONS_WEBHOOK_SECRET (reuses
                 # STRIPE_SECRET_KEY and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
                 # if already set up for the other modules).

1. Variables de entorno

STRIPE_SECRET_KEY y NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY se comparten entre los tres módulos de Stripe: definilas una sola vez. Además, agregá:

VariableDónde conseguirla
STRIPE_SUBSCRIPTIONS_WEBHOOK_SECRETDashboard → Developers → Webhooks (creá un tercer endpoint, separado)

2. Webhooks

Agregá un endpoint separado en el Stripe Dashboard que apunte a:

https://yourdomain.com/api/webhooks/stripe-subscriptions

Suscribilo como mínimo a:

  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted — tu señal real de "expiración"
  • customer.subscription.trial_will_end
  • customer.subscription.paused
  • customer.subscription.resumed
  • invoice.paid — tu señal real de "renovación" para los cobros recurrentes
  • invoice.payment_failed

Para probar en local:

stripe listen --forward-to localhost:3000/api/webhooks/stripe-subscriptions

La lógica de negocio de lo que pasa con cada evento va dentro de stripeSubscriptionsService.handleWebhookEvent en service.ts: la única parte de ese archivo pensada para extenderse a medida que tu app crece.

3. Uso

Crear una suscripción (con o sin trial)

import { createSubscription } from "@/lib/stripe-subscriptions/actions";

const result = await createSubscription({
  customerId,
  priceId,
  trialPeriodDays: 14, // omit for no trial
});

if (result.success && result.data.clientSecret) {
  // No trial (or a card needing 3D Secure): confirm the first charge client-side.
}

Del lado del cliente, solo cuando clientSecret no es null:

"use client";
import { confirmSubscriptionPayment } from "@/lib/stripe-subscriptions/client";

await confirmSubscriptionPayment(clientSecret, paymentMethodId);

Una suscripción creada con un trial y sin clientSecret ya está en trialing: no hay nada que confirmar hasta que termine el trial y Stripe la cobre automáticamente (se maneja mediante los eventos de webhook invoice.paid / invoice.payment_failed).

Leer la suscripción / el estado

import { getSubscription, getSubscriptionStatus } from "@/lib/stripe-subscriptions/actions";

const sub = await getSubscription(subscriptionId);
const status = await getSubscriptionStatus(subscriptionId);

Cancelar

import { cancelSubscription } from "@/lib/stripe-subscriptions/actions";

await cancelSubscription({ subscriptionId }); // immediate
await cancelSubscription({ subscriptionId, atPeriodEnd: true }); // at period end

Pausar / reanudar

import { pauseSubscription, resumeSubscription } from "@/lib/stripe-subscriptions/actions";

await pauseSubscription(subscriptionId);
await resumeSubscription(subscriptionId);

Upgrade / downgrade / cambio de plan

Los tres son la misma operación: cambiar el price del ítem de la suscripción. Decidí la etiqueta ("upgrade" vs "downgrade") en tu UI según la diferencia de precio; a la función en sí no le importa:

import { changeSubscriptionPlan } from "@/lib/stripe-subscriptions/actions";

await changeSubscriptionPlan({
  subscriptionId,
  newPriceId,
  prorationBehavior: "create_prorations", // default; see Stripe docs for alternatives
});

Cambiar la cantidad (por ejemplo, seats)

import { changeSubscriptionQuantity } from "@/lib/stripe-subscriptions/actions";

await changeSubscriptionQuantity({ subscriptionId, quantity: 5 });

Información de renovación / expiración

No existe una acción de "renovar" en la API de Stripe: la renovación ocurre automáticamente con el ciclo de facturación. Estas dos leen el estado actual en su lugar:

import { getRenewalInfo, getExpirationInfo } from "@/lib/stripe-subscriptions/actions";

const renewal = await getRenewalInfo(subscriptionId);
// { willRenew, currentPeriodEnd, nextPaymentAttempt }

const expiration = await getExpirationInfo(subscriptionId);
// { cancelAtPeriodEnd, expiresAt, status }

Para los eventos de renovación/expiración en el momento en que ocurren, escuchá invoice.paid (renovada) y customer.subscription.deleted (expirada) en el handler del webhook.

Manejo de errores

Cada función de actions.ts y client.ts devuelve:

{ success: true, data: T } | { success: false, error: string }

Verificá siempre result.success antes de leer result.data: nada lanza errores a través del límite del módulo.

Notas

  • service.ts es el único archivo que importa el paquete stripe o lee STRIPE_SECRET_KEY / STRIPE_SUBSCRIPTIONS_WEBHOOK_SECRET.
  • Este módulo no crea Products/Prices ni Customers: se asume que ambos ya existen.
  • La pausa usa pause_collection: { behavior: "void" } (no se generan facturas mientras está pausada, no se debe nada por ese tiempo). Revisá la documentación de Stripe si preferís el comportamiento mark_uncollectible o keep_as_draft.