July 7, 2025
O. Wolfson
This article expands on our Stripe + Next.js 15 integration, showing how to support recurring payments (subscriptions) and multi-currency checkout. You’ll learn best practices, see example code, and understand the tradeoffs in international SaaS/e-commerce pricing.
With the basics in place (one-time payments using Stripe Elements), most modern apps will want to support:
Stripe supports both with a secure, scalable API. Here’s how to add these features to your Next.js 15 (App Router) project.
mode: "subscription").A. Create Subscription Prices in Stripe:
Type: Recurring and a currency (USD, THB, EUR, etc.).B. API Route for Subscription Checkout:
Create a new API route, e.g. app/api/create-subscription-session/route.ts:
tsimport { NextResponse } from "next/server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { priceId, customerEmail } = await request.json();
try {
const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
success_url:
"https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "https://yourdomain.com/cancel",
customer_email: customerEmail,
});
return NextResponse.json({ url: session.url });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 400 });
}
}
C. Client Component to Subscribe:
tsx"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
export default function SubscribeButton({
priceId,
customerEmail,
}: {
priceId: string;
customerEmail?: string;
}) {
const [loading, setLoading] = useState(false);
const handleSubscribe = async () => {
setLoading(true);
const res = await fetch("/api/create-subscription-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId, customerEmail }),
});
const { url, error } = await res.json();
setLoading(false);
if (url) {
window.location.href = url;
} else {
alert(error ?? "Subscription failed.");
}
};
return (
<Button onClick={handleSubscribe} disabled={loading}>
{loading ? "Redirecting..." : "Subscribe"}
</Button>
);
}
Create all needed prices in Stripe:
price_xxx_usd)price_xxx_thb)Let users choose their currency (dropdown/radio):
tsxconst priceIds = {
usd: "price_xxx_usd",
thb: "price_xxx_thb",
// more...
};
const [currency, setCurrency] = useState<"usd" | "thb">("usd");
<SubscribeButton priceId={priceIds[currency]} />;
tsx"use client";
import { useState } from "react";
import SubscribeButton from "@/components/subscribe-button";
const priceIds = {
usd: "price_xxx_usd",
thb: "price_xxx_thb",
};
export default function SubscriptionPage() {
const [currency, setCurrency] = useState<"usd" | "thb">("usd");
return (
<div>
<h1>Choose your plan</h1>
<div>
<label>
<input
type="radio"
value="usd"
checked={currency === "usd"}
onChange={() => setCurrency("usd")}
/>
USD $10/month
</label>
<label>
<input
type="radio"
value="thb"
checked={currency === "thb"}
onChange={() => setCurrency("thb")}
/>
THB 350/month
</label>
</div>
<SubscribeButton priceId={priceIds[currency]} />
</div>
);
}
customer.subscription.created, updated, deleted events to update user status in your DB.Questions or need code for specific regions, currencies, or subscription models? Just ask.