- Go to https://stripe.com and create an account
- Complete onboarding (you can skip to test mode)
- In the Stripe Dashboard, go to Developers → API keys
- You will see two keys:
- Publishable key (starts with
pk_test_...orpk_live_...) - Secret key (starts with
sk_test_...orsk_live_...) - Important: Use
testkeys during development!
- Publishable key (starts with
- In the Dashboard, go to Developers → Webhooks
- Click Add endpoint
- URL:
https://your-domain.com/api/webhooks/stripe(use ngrok for local testing) - Events to listen for:
payment_intent.succeededpayment_intent.payment_failedpayment_intent.canceled
npm install stripe @stripe/stripe-jsAdd to your .env.local file:
# Stripe Keys
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_publishable_key_here
STRIPE_SECRET_KEY=your_secret_key_here
# Webhook Secret (get this after setting up the webhook in Stripe)
STRIPE_WEBHOOK_SECRET=your_webhook_secret_here- Never commit secret keys to Git!
- Use test keys during development
- Switch to live keys only in production
src/
├── lib/
│ └── stripe/
│ ├── client.ts # Stripe client for frontend
}
export const stripePromise = loadStripe(stripePublishableKey)
src/lib/stripe/server.ts
import Stripe from 'stripe'
const stripeSecretKey = process.env.STRIPE_SECRET_KEY!
if (!stripeSecretKey) {
throw new Error('Missing STRIPE_SECRET_KEY')
}
export const stripe = new Stripe(stripeSecretKey, {
apiVersion: '2024-11-20.acacia',
})src/app/api/payment-intent/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { stripe } from '@/lib/stripe/server'
export async function POST(request: NextRequest) {
try {
const { amount, currency = 'usd', customerEmail, metadata } = await request.json()
// Create Payment Intent
const paymentIntent = await stripe.paymentIntents.create({
amount: amount, // in cents
currency: currency.toLowerCase(),
customer_email: customerEmail,
metadata: metadata || {},
automatic_payment_methods: {
enabled: true,
},
})
return NextResponse.json({
clientSecret: paymentIntent.client_secret,
paymentIntentId: paymentIntent.id,
})
} catch (error: any) {
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
}src/app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { stripe } from '@/lib/stripe/server'
import { createClient } from '@/lib/supabase/server'
export async function POST(request: NextRequest) {
const body = await request.text()
const signature = request.headers.get('stripe-signature')!
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
let event
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
} catch (err: any) {
return NextResponse.json(
{ error: `Webhook signature verification failed: ${err.message}` },
{ status: 400 }
)
}
// Handle events
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object
// Update booking in the database as paid
const supabase = await createClient()
await supabase
.from('bookings')
.update({ status: 'confirmed' })
.eq('payment_intent_id', paymentIntent.id)
console.log('✅ Payment succeeded:', paymentIntent.id)
break
case 'payment_intent.payment_failed':
console.log('❌ Payment failed:', event.data.object.id)
break
default:
console.log(`Unhandled event type: ${event.type}`)
}
return NextResponse.json({ received: true })
}In the BookingForm.tsx component, you will need to:
- Create Payment Intent before showing the payment form
- Use
@stripe/react-stripe-jsfor form elements - Confirm payment when the user submits
Additional dependency:
npm install @stripe/react-stripe-jsMain changes:
- Create Payment Intent when moving to the payment step
- Use Stripe
<Elements> - Use
<PaymentElement>for the card form - Confirm payment with
stripe.confirmPayment() - Update booking with
payment_intent_idin the database
1. User fills in details → Clicks "Continue to Payment"
2. Frontend calls `/api/payment-intent` → Receives `clientSecret`
3. Shows Stripe form (PaymentElement)
4. User enters card → Clicks "Confirm Payment"
5. Frontend confirms with Stripe → `stripe.confirmPayment()`
6. Stripe processes payment
7. Webhook receives event → Updates booking in the database
8. Frontend redirects to confirmation page
Stripe provides test cards:
| Card Number | Description |
|---|---|
4242 4242 4242 4242 |
Success |
4000 0000 0000 9995 |
Declined (insufficient) |
4000 0025 0000 3155 |
Requires 3D Secure |
Test Codes:
- CVV: Any 3 digits
- Expiry: Any future date
- ZIP: Any
- Never expose the Secret Key on the frontend
- Always validate webhooks with signature
- Use HTTPS in production
- Validate data on the backend before creating Payment Intent
- Store only
payment_intent_idin the database (never card data)
- Test thoroughly in test mode
- Set up webhooks in production
- Implement retry logic for failures
- Add payment logs
- Create a payment dashboard (optional)
Q: Can I test locally?
A: Yes! Use ngrok or stripe CLI to test webhooks locally.
Q: How do I switch from test to production?
A: Just change the API keys in .env.local (replace pk_test_ with pk_live_).
Q: Do I need SSL for webhooks?
A: Yes, Stripe requires HTTPS. Use ngrok during development.
Ready to implement? Say "yes" and I will create all the files! 🚀