| name | stripe-payments |
| description | Use when integrating Stripe payments, subscriptions, webhooks, or billing into web applications. Covers Checkout, Payment Intents, subscriptions, customer portal, and webhook handling. |
Stripe Payments
Setup
bun add stripe @stripe/stripe-js
import Stripe from 'stripe'
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia',
typescript: true,
})
Checkout Session (One-time Payment)
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: 'payment',
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
metadata: { userId },
customer_email: 'user@example.com',
})
return NextResponse.json({ url: session.url })
}
const response = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ priceId: 'price_xxx' }),
})
const { url } = await response.json()
window.location.href = url
Subscription Checkout
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: 'price_monthly_xxx', quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
subscription_data: {
trial_period_days: 14,
metadata: { userId },
},
})
Customer Portal
export async function POST(req: Request) {
const { customerId } = await req.json()
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_URL}/settings`,
})
return NextResponse.json({ url: session.url })
}
Webhooks
import { headers } from 'next/headers'
import { stripe } from '@/lib/stripe'
import Stripe from 'stripe'
export async function POST(req: Request) {
const body = await req.text()
const sig = (await headers()).get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
} catch (err) {
return new Response(`Webhook Error: ${err}`, { status: 400 })
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.CheckoutSession
const userId = session.metadata?.userId
break
}
case 'customer.subscription.created':
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription
break
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription
break
}
case 'invoice.payment_failed': {
break
}
}
return new Response(null, { status: 200 })
}
export const config = { api: { bodyParser: false } }
Payment Intent (Custom UI)
const paymentIntent = await stripe.paymentIntents.create({
amount: 2000,
currency: 'brl',
metadata: { orderId: '123' },
})
return NextResponse.json({ clientSecret: paymentIntent.client_secret })
import { loadStripe } from '@stripe/stripe-js'
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js'
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!)
function CheckoutForm({ clientSecret }: { clientSecret: string }) {
const stripe = useStripe()
const elements = useElements()
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!stripe || !elements) return
const { error } = await stripe.confirmPayment({
elements,
confirmParams: { return_url: `${window.location.origin}/success` },
})
if (error) console.error(error.message)
}
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
<button type="submit">Pay</button>
</form>
)
}
export function PaymentPage({ clientSecret }: { clientSecret: string }) {
return (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm clientSecret={clientSecret} />
</Elements>
)
}
Useful Utilities
const formatAmount = (amount: number, currency = 'brl') =>
new Intl.NumberFormat('pt-BR', { style: 'currency', currency: currency.toUpperCase() })
.format(amount / 100)
async function getOrCreateCustomer(email: string, userId: string) {
const existing = await stripe.customers.list({ email, limit: 1 })
if (existing.data.length > 0) return existing.data[0]
return stripe.customers.create({ email, metadata: { userId } })
}
Local Testing
brew install stripe/stripe-cli/stripe
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger checkout.session.completed
stripe trigger customer.subscription.created
Environment Variables
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
NEXT_PUBLIC_URL=http://localhost:3000