Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Crea una Checkout Session en el servidor y luego redirige al cliente a la página de checkout alojada de Stripe. Stripe se encarga de toda la UI de pago, el cumplimiento PCI y la visualización de métodos de pago.
Crea una Server Action para generar la sesión:
// app/actions/checkout.ts
"use server";
import { stripe } from "@/lib/stripe";
import { redirect } from "next/navigation";
export async function createCheckoutSession(priceId: string) {
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
});
redirect(session.url!);
}O usa un Route Handler:
// app/api/checkout/route.ts
import { stripe } from "@/lib/stripe";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const { priceId } = await request.json();
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
});
return NextResponse.json({ url: session.url });
}// app/buy/page.tsx
import { createCheckoutSession } from "@/app/actions/checkout";
export default function BuyPage() {
return (
<div className="max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold mb-4">Curso Premium</h1>
<p className="text-gray-600 mb-6">
Compra única para acceso de por vida.
</p>
<form action={createCheckoutSession.bind(null, "price_xxx123")}>
<button
type="submit"
className="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700"
>
Comprar por $49
</button>
</form>
</div>
);
}// app/success/page.tsx
import { stripe } from "@/lib/stripe";
interface SuccessPageProps {
searchParams: Promise<{ session_id?: string }>;
}
export default async function SuccessPage({ searchParams }: SuccessPageProps) {
const { session_id } = await searchParams;
if (!session_id) {
return <p>Sesión no válida.</p>;
}
const session = await stripe.checkout.sessions.retrieve(session_id, {
expand: ["line_items", "customer"],
});
return (
<div className="max-w-md mx-auto p-8 text-center">
<h1 className="text-2xl font-bold mb-4">¡Pago exitoso!</h1>
<p className="text-gray-600">
Gracias por tu compra. Tu pago de{" "}
{(session.amount_total! / 100).toFixed(2)}{" "}
{session.currency?.toUpperCase()} ha sido recibido.
</p>
<p className="mt-4 text-sm text-gray-500">
ID de sesión: {session.id}
</p>
</div>
);
}stripe.checkout.sessions.create genera una sesión en los servidores de Stripe y devuelve una URL. Al redirigir al cliente a esa URL, se abre la página de checkout alojada por Stripe.{CHECKOUT_SESSION_ID} es una variable de plantilla que Stripe reemplaza con el ID real de la sesión al redirigir de vuelta a tu success_url.mode determina el tipo de pago: "payment" para pagos únicos, "subscription" para recurrentes o "setup" para guardar una tarjeta sin cobrar.line_items puede referenciar objetos Price existentes (creados en el Dashboard) o usar price_data inline para precios dinámicos.Datos de precio inline (sin objeto Price precreado):
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: "Widget personalizado",
description: "Un widget único",
},
unit_amount: 2999, // $29.99 en centavos
},
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/shop`,
});Asociar a un cliente existente:
const session = await stripe.checkout.sessions.create({
mode: "payment",
customer: "cus_xxx",
line_items: [{ price: "price_xxx", quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
});Varios line items:
line_items: [
{ price: "price_basic", quantity: 2 },
{ price: "price_addon", quantity: 1 },
],stripe.checkout.sessions.create devuelve Promise<Stripe.Checkout.Session>.session.url está tipada como string | null. Es null solo en el modo de checkout embebido, así que la aserción ! es segura para flujos basados en redirección.Stripe.Checkout.SessionCreateParams para tipar objetos de configuración si los construyes dinámicamente.import type Stripe from "stripe";
const params: Stripe.Checkout.SessionCreateParams = {
mode: "payment",
line_items: [{ price: priceId, quantity: 1 }],
success_url: "...",
cancel_url: "...",
};session.url expira después de 24 horas. No la guardes para usarla más tarde.amount_total está en la unidad más pequeña de la moneda (centavos para USD). Divídelo entre 100 antes de mostrarlo.redirect() de Next.js en una Server Action, lanza internamente un error NEXT_REDIRECT. Es el comportamiento esperado: no lo envuelvas en un try/catch que capture el error.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Stripe Checkout (redirección) | Código mínimo, cumple PCI, admite más de 40 métodos de pago | Personalización limitada de la UI |
| Embedded Checkout | Permanece en tu sitio mediante iframe | Opciones de estilo aún limitadas |
| PaymentIntent + PaymentElement | Control total de la UI | Más código, más responsabilidad |
| Payment Links | Sin código | Sin control programático |
redirect() para enviar al usuario a StripeStripe reemplaza {CHECKOUT_SESSION_ID} con el ID real de la sesión al redirigir al cliente de vuelta a tu sitio. Luego puedes recuperar la sesión en el servidor para verificar los detalles del pago.
"payment" - pago único"subscription" - facturación recurrente"setup" - guardar una tarjeta sin cobrarlaUn usuario podría navegar manualmente a tu URL de éxito sin pagar. Verifica siempre el pago mediante webhooks (checkout.session.completed) o recuperando la sesión en el servidor y comprobando su payment_status.
line_items: [{
price_data: {
currency: "usd",
product_data: { name: "Custom Widget" },
unit_amount: 2999, // $29.99 en centavos
},
quantity: 1,
}]Stripe almacena los importes en la unidad más pequeña de la moneda (centavos para USD). Así, 2999 significa $29.99. Divide siempre entre 100 para mostrar dólares.
redirect() lanza internamente un error NEXT_REDIRECT. Si tu bloque catch lo captura, la redirección nunca ocurre. Llama a redirect() fuera del try/catch o relanza explícitamente los errores de redirección.
const session = await stripe.checkout.sessions.create({
mode: "payment",
customer: "cus_xxx",
line_items: [{ price: "price_xxx", quantity: 1 }],
success_url: "...",
cancel_url: "...",
});La URL de la Checkout Session expira después de 24 horas. Genera siempre una sesión nueva cuando el usuario inicia el checkout; nunca guardes la URL para usarla más tarde.
import type Stripe from "stripe";
const params: Stripe.Checkout.SessionCreateParams = {
mode: "payment",
line_items: [{ price: priceId, quantity: 1 }],
success_url: "...",
cancel_url: "...",
};Es null solo cuando usas el modo de checkout embebido (donde no se necesita URL de redirección). En flujos basados en redirección, siempre es un string, lo que hace segura la aserción no nula !.
Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥