Integrar pagos recurrentes con Stripe en Next.js parece sencillo hasta que llegas a los webhooks — y ahí es donde el 90 % de los tutoriales en inglés se quedan cortos o dejan agujeros de seguridad. Esta guía, en castellano y con App Router, te lleva de cero a una suscripción funcionando bien hecha: Checkout, webhooks verificados y Customer Portal.
✦ Lo que vas a montar
Un flujo completo de suscripción: el usuario pulsa «Suscribirme» → va a Stripe Checkout → paga → un webhook actualiza tu base de datos → el usuario gestiona o cancela desde el Customer Portal de Stripe. Sin construir formularios de tarjeta tú mismo.
Índice
Requisitos previos
- Una app Next.js 14+ con App Router.
- Una cuenta de Stripe (modo test activado).
- La Stripe CLI para reenviar webhooks a
localhostmientras desarrollas. - Una base de datos donde guardar el estado de la suscripción (Supabase, por ejemplo).
Crear productos y precios
En el dashboard de Stripe → Productos, crea uno (p. ej. «Plan Pro»), y dentro un precio recurrente (mensual o anual). Copia el price_id (algo como price_1Q...). Ese ID es lo que pasas al crear el Checkout — no el importe a mano.
Instalar y configurar el SDK
npm install stripe
# .env.local
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_APP_URL=http://localhost:3000
Centraliza el cliente de Stripe en un único archivo para reutilizarlo:
// lib/stripe.ts
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2025-05-28.basil",
});
Sesión de Checkout (App Router)
Crea una Route Handler. Cuando el usuario pulsa el botón, llamas a este endpoint y rediriges a la URL que devuelve Stripe:
// app/api/checkout/route.ts
import { stripe } from "@/lib/stripe";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const { priceId, userId } = await req.json();
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
// Vincula la sesión a TU usuario para reconocerlo en el webhook:
client_reference_id: userId,
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?ok=1`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/precios`,
});
return NextResponse.json({ url: session.url });
}
✦ Clave
El client_reference_id (o metadata) es cómo unes la suscripción de Stripe con el usuario de tu base de datos. Sin esto, cuando llegue el webhook no sabrás a quién dar acceso.
Webhooks: la parte crítica
Nunca des acceso premium en la página de éxito del navegador. Esa redirección puede falsearse o no llegar. La verdad del pago vive en el webhook.
Dos detalles que rompen a todo el mundo en App Router: necesitas el cuerpo crudo (raw body) para verificar la firma, y debes leerlo con req.text(), no req.json():
// app/api/webhooks/stripe/route.ts
import { stripe } from "@/lib/stripe";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const body = await req.text(); // raw, sin parsear
const sig = req.headers.get("stripe-signature")!;
let event;
try {
event = stripe.webhooks.constructEvent(
body, sig, process.env.STRIPE_WEBHOOK_SECRET!
);
} catch {
return new NextResponse("Firma inválida", { status: 400 });
}
switch (event.type) {
case "checkout.session.completed": {
const s = event.data.object;
// s.client_reference_id = tu userId → activa el plan en tu BBDD
break;
}
case "customer.subscription.updated":
case "customer.subscription.deleted": {
// actualiza estado: activa / cancelada / morosa
break;
}
}
return NextResponse.json({ received: true });
}
En local, reenvía los eventos con la CLI:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
La CLI te imprime el whsec_... que va en STRIPE_WEBHOOK_SECRET. En producción, el secreto lo obtienes al registrar el endpoint en el dashboard.
Todo esto ya montado.
IndiePack trae Stripe (y LemonSqueezy) integrado con webhooks verificados, Customer Portal y la BBDD conectada. En castellano.
Ver el stack →Customer Portal: gestión y bajas
No construyas tú la pantalla de «cambiar de plan / cancelar». Stripe te la da hecha con el Billing Portal. Generas una sesión y rediriges:
// app/api/portal/route.ts
import { stripe } from "@/lib/stripe";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const { customerId } = await req.json(); // guardado en tu BBDD
const portal = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
});
return NextResponse.json({ url: portal.url });
}
Guarda el stripe_customer_id en tu base de datos en cuanto se cree la suscripción. Lo necesitas tanto para el portal como para reconciliar webhooks.
Los 5 errores más comunes
- Dar acceso desde la página de éxito. Hazlo solo desde el webhook verificado.
- Usar
req.json()en el webhook. Rompe la verificación de firma. Usareq.text(). - No guardar
customer_idnisubscription_id. Sin ellos no puedes gestionar bajas ni renovaciones. - Ignorar
customer.subscription.deleted. Si no lo escuchas, sigues dando premium a quien ya canceló. - Probar solo el «happy path». Usa las tarjetas de test de fallo de Stripe (pagos rechazados, 3D Secure) antes de lanzar.
¿Y el IVA?
Stripe es una pasarela, no un merchant of record: el IVA lo gestionas tú (o activas Stripe Tax). Si vendes a particulares por toda la UE y no quieres pelearte con el IVA por país, quizá te interese más LemonSqueezy. Lo comparamos en LemonSqueezy vs Stripe, y la parte fiscal española la detallamos en cómo facturar tu SaaS como autónomo.
Preguntas frecuentes
¿Funciona con el App Router?
Sí. Usas Route Handlers para Checkout y webhooks. El único detalle es leer el raw body con req.text() en el webhook.
¿Necesito un formulario de tarjeta propio?
No. Con Stripe Checkout, Stripe aloja el formulario de pago (PCI-compliant). Tú solo rediriges.
¿Cómo pruebo los pagos sin dinero real?
En modo test, usa la tarjeta 4242 4242 4242 4242 con cualquier fecha futura y CVC.
Conclusión
Integrar Stripe con suscripciones en Next.js son cuatro piezas: Checkout para cobrar, webhooks verificados para saber la verdad del pago, tu BBDD como fuente del estado, y el Customer Portal para que el usuario se gestione solo. Hazlo bien una vez y lo reutilizas en cada proyecto.
O sáltate el montaje: IndiePack trae todo este flujo ya implementado y probado, con webhooks, portal y base de datos conectada — en castellano y por un pago único.
Pagos resueltos desde el minuto uno.
Stripe y LemonSqueezy integrados, webhooks verificados, Customer Portal. Copia, configura y a vender.
Conseguir IndiePack — 250€ →