Implement production-ready payment processing with Stripe. Learn checkout sessions, webhooks, subscriptions, customer portals, and secure payment handling with Next.js App Router and TypeScript.
# Stripe Payments Integration with Next.js
Build secure, production-ready payment processing using Stripe with Next.js. This guide covers one-time payments, subscriptions, webhooks, and customer management.
## Setting Up Stripe
### Environment Configuration
```typescript
// lib/stripe.ts
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-11-20.acacia",
typescript: true,
});
// Types for your products
export interface PricingPlan {
id: string;
name: string;
description: string;
priceId: string;
price: number;
interval: "month" | "year";
features: string[];
}
export const PRICING_PLANS: PricingPlan[] = [
{
id: "pro",
name: "Pro",
description: "For growing teams",
priceId: process.env.STRIPE_PRO_PRICE_ID!,
price: 29,
interval: "month",
features: ["Unlimited projects", "Priority support", "Advanced analytics"],
},
{
id: "enterprise",
name: "Enterprise",
description: "For large organizations",
priceId: process.env.STRIPE_ENTERPRISE_PRICE_ID!,
price: 99,
interval: "month",
features: ["Everything in Pro", "Custom integrations", "Dedicated support"],
},
];
```
### Creating Checkout Sessions
```typescript
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
export async function POST(req: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { priceId, successUrl, cancelUrl } = await req.json();
// Find or create customer
const customers = await stripe.customers.list({
email: session.user.email,
limit: 1,
});
let customerId = customers.data[0]?.id;
if (!customerId) {
const customer = await stripe.customers.create({
email: session.user.email,
name: session.user.name || undefined,
metadata: { userId: session.user.id },
});
customerId = customer.id;
}
const checkoutSession = await stripe.checkout.sessions.create({
customer: customerId,
mode: "subscription",
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
success_url: successUrl || `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
cancel_url: cancelUrl || `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,
subscription_data: {
metadata: { userId: session.user.id },
},
allow_promotion_codes: true,
});
return NextResponse.json({ url: checkoutSession.url });
} catch (error) {
console.error("Checkout error:", error);
return NextResponse.json(
{ error: "Failed to create checkout session" },
{ status: 500 }
);
}
}
```
### Webhook Handler
```typescript
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { headers } from "next/headers";
import { db } from "@/lib/db";
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = headers().get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (error) {
console.error("Webhook signature verification failed");
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
try {
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
await handleCheckoutComplete(session);
break;
}
case "customer.subscription.updated": {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionUpdate(subscription);
break;
}
case "customer.subscription.deleted": {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionCanceled(subscription);
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentFailed(invoice);
break;
}
}
} catch (error) {
console.error("Webhook handler error:", error);
return NextResponse.json({ error: "Webhook handler failed" }, { status: 500 });
}
return NextResponse.json({ received: true });
}
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
);
await db.user.update({
where: { id: subscription.metadata.userId },
data: {
stripeCustomerId: session.customer as string,
stripeSubscriptionId: subscription.id,
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
async function handleSubscriptionUpdate(subscription: Stripe.Subscription) {
await db.user.update({
where: { stripeSubscriptionId: subscription.id },
data: {
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
```
### Customer Portal
```typescript
// app/api/billing/portal/route.ts
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { db } from "@/lib/db";
export async function POST() {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const user = await db.user.findUnique({
where: { id: session.user.id },
select: { stripeCustomerId: true },
});
if (!user?.stripeCustomerId) {
return NextResponse.json({ error: "No billing account" }, { status: 400 });
}
const portalSession = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
});
return NextResponse.json({ url: portalSession.url });
}
```
### Client-Side Integration
```typescript
// components/PricingCard.tsx
"use client";
import { useState } from "react";
import { type PricingPlan } from "@/lib/stripe";
export function PricingCard({ plan }: { plan: PricingPlan }) {
const [loading, setLoading] = useState(false);
const handleSubscribe = async () => {
setLoading(true);
try {
const response = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId: plan.priceId }),
});
const { url } = await response.json();
if (url) window.location.href = url;
} catch (error) {
console.error("Subscription error:", error);
} finally {
setLoading(false);
}
};
return (
<div className="rounded-2xl border p-8">
<h3 className="text-xl font-bold">{plan.name}</h3>
<p className="text-gray-600">{plan.description}</p>
<div className="my-6">
<span className="text-4xl font-bold">${plan.price}</span>
<span className="text-gray-500">/{plan.interval}</span>
</div>
<ul className="mb-8 space-y-3">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center gap-2">
<CheckIcon className="h-5 w-5 text-green-500" />
{feature}
</li>
))}
</ul>
<button
onClick={handleSubscribe}
disabled={loading}
className="w-full rounded-lg bg-blue-600 py-3 text-white hover:bg-blue-700 disabled:opacity-50"
>
{loading ? "Processing..." : "Get Started"}
</button>
</div>
);
}
```
This complete Stripe integration provides secure payment processing with proper webhook handling, customer management, and subscription lifecycle support.This stripe prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their stripe implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For stripe projects, consider mentioning your framework version, coding style, and any specific libraries you're using.