Payments
Accept payments with Stripe
Payments
Integrate Stripe to accept one-time payments and subscriptions.
Setup
1. Install Stripe
pnpm add stripe @stripe/stripe-js2. Configure Environment Variables
STRIPE_PUBLIC_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...3. Initialize Stripe
// lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});One-Time Payments
Create Payment Intent
// app/api/create-payment-intent/route.ts
import { stripe } from '@/lib/stripe';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { amount } = await request.json();
try {
const paymentIntent = await stripe.paymentIntents.create({
amount: amount * 100, // Convert to cents
currency: 'usd',
automatic_payment_methods: {
enabled: true,
},
});
return NextResponse.json({
clientSecret: paymentIntent.client_secret,
});
} catch (error) {
console.error('Payment intent error:', error);
return NextResponse.json(
{ error: 'Failed to create payment intent' },
{ status: 500 }
);
}
}Payment Form
'use client';
import { useState } from 'react';
import {
PaymentElement,
Elements,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!);
function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const [isProcessing, setIsProcessing] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
setIsProcessing(true);
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/payment/success`,
},
});
if (error) {
console.error('Payment error:', error);
}
setIsProcessing(false);
};
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
<button type="submit" disabled={!stripe || isProcessing}>
{isProcessing ? 'Processing...' : 'Pay Now'}
</button>
</form>
);
}
export function PaymentPage({ clientSecret }: { clientSecret: string }) {
return (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm />
</Elements>
);
}Subscriptions
See the Stripe Subscriptions Tutorial for detailed implementation.
Webhook Events
Handle Stripe webhook events:
// app/api/webhooks/stripe/route.ts
import { stripe } from '@/lib/stripe';
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (error) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 400 }
);
}
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log('Payment succeeded:', paymentIntent.id);
// Update database, send confirmation email, etc.
break;
case 'payment_intent.payment_failed':
const failedPayment = event.data.object;
console.log('Payment failed:', failedPayment.id);
// Notify user, retry payment, etc.
break;
case 'invoice.payment_succeeded':
const invoice = event.data.object;
console.log('Invoice paid:', invoice.id);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
}Products and Prices
Create Product
const product = await stripe.products.create({
name: 'Premium Plan',
description: 'All premium features',
images: ['https://yoursite.com/product-image.jpg'],
metadata: {
category: 'subscription',
},
});Create Price
const price = await stripe.prices.create({
product: product.id,
unit_amount: 2000, // $20.00
currency: 'usd',
recurring: {
interval: 'month',
},
});Customer Portal
Allow customers to manage billing:
// app/api/create-portal-session/route.ts
import { stripe } from '@/lib/stripe';
import { auth } from '@/lib/auth';
import { db } from '@/prisma';
import { NextResponse } from 'next/server';
export async function POST() {
const session = await auth();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const user = await db.user.findUnique({
where: { id: session.user.id },
});
const portalSession = await stripe.billingPortal.sessions.create({
customer: user!.stripeCustomerId!,
return_url: `${process.env.NEXT_PUBLIC_URL}/dashboard`,
});
return NextResponse.json({ url: portalSession.url });
}Testing
Use Stripe test cards:
- Success:
4242 4242 4242 4242 - Decline:
4000 0000 0000 0002 - 3D Secure:
4000 0025 0000 3155
Pricing Table
Display pricing options:
export function PricingTable({ prices }) {
return (
<div className="grid grid-cols-3 gap-8">
{prices.map((price) => (
<div key={price.id} className="border rounded-lg p-6">
<h3 className="text-2xl font-bold">{price.product.name}</h3>
<p className="text-4xl font-bold mt-4">
${price.unit_amount / 100}
<span className="text-lg text-gray-500">/mo</span>
</p>
<ul className="mt-6 space-y-4">
{price.product.features.map((feature) => (
<li key={feature} className="flex items-center">
<CheckIcon className="w-5 h-5 text-green-500 mr-2" />
{feature}
</li>
))}
</ul>
<button className="w-full mt-8 btn-primary">
Subscribe
</button>
</div>
))}
</div>
);
}Security Best Practices
- Never expose secret keys: Use environment variables
- Verify webhook signatures: Prevent fake events
- Use HTTPS: Required for production
- Implement idempotency: Handle duplicate events
- Store minimal data: Let Stripe handle sensitive info
- Use customer portal: Don't build custom billing UI
Next Steps
- Set up Webhooks in Stripe Dashboard
- Learn about Error Handling
- Explore Customer Support