Back to Blueprints
paymentsmedium3-4 hours

Stripe Checkout Integration

Implement Stripe Checkout for one-time payments with webhooks and order fulfillment

stripepaymentscheckoute-commerce

Overview

Integrate Stripe Checkout for secure payment processing. This blueprint covers creating checkout sessions, handling webhooks for payment confirmation, and fulfilling orders.

Prerequisites

  • Stripe account (test mode for development)
  • Product/price created in Stripe Dashboard
  • Database for storing orders

Steps

1. Install Stripe SDK

npm install stripe @stripe/stripe-js

2. Environment Variables

STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...

3. Stripe Server Client

// src/lib/stripe.ts
import Stripe from 'stripe'

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-06-20',
})

4. Create Checkout Session API

// src/app/api/checkout/route.ts
import { stripe } from '@/lib/stripe'
import { auth } from '@/lib/auth'

export async function POST(req: Request) {
  const session = await auth()
  if (!session?.user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { priceId, quantity = 1 } = await req.json()

  const checkoutSession = await stripe.checkout.sessions.create({
    mode: 'payment',
    payment_method_types: ['card'],
    line_items: [
      {
        price: priceId,
        quantity,
      },
    ],
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/checkout/cancel`,
    customer_email: session.user.email!,
    metadata: {
      userId: session.user.id,
    },
  })

  return Response.json({ url: checkoutSession.url })
}

5. Checkout Button Component

// src/components/CheckoutButton.tsx
"use client"
import { useState } from 'react'

interface Props {
  priceId: string
  children: React.ReactNode
}

export function CheckoutButton({ priceId, children }: Props) {
  const [loading, setLoading] = useState(false)

  const handleCheckout = async () => {
    setLoading(true)
    try {
      const res = await fetch('/api/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ priceId }),
      })
      const { url } = await res.json()
      window.location.href = url
    } catch (error) {
      console.error('Checkout error:', error)
    } finally {
      setLoading(false)
    }
  }

  return (
    <button onClick={handleCheckout} disabled={loading}>
      {loading ? 'Loading...' : children}
    </button>
  )
}

6. Webhook Handler

// src/app/api/webhooks/stripe/route.ts
import { stripe } from '@/lib/stripe'
import { headers } from 'next/headers'
import { db } from '@/lib/db'

export async function POST(req: Request) {
  const body = await req.text()
  const signature = headers().get('stripe-signature')!

  let event

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    )
  } catch (err) {
    console.error('Webhook signature verification failed')
    return Response.json({ error: 'Invalid signature' }, { status: 400 })
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object
      await fulfillOrder(session)
      break
    }
    case 'payment_intent.payment_failed': {
      const paymentIntent = event.data.object
      console.log('Payment failed:', paymentIntent.id)
      break
    }
  }

  return Response.json({ received: true })
}

async function fulfillOrder(session: any) {
  const userId = session.metadata.userId

  await db.order.create({
    data: {
      userId,
      stripeSessionId: session.id,
      amount: session.amount_total,
      status: 'completed',
    },
  })

  // Add your fulfillment logic here:
  // - Send confirmation email
  // - Grant access to product
  // - Update inventory
}

7. Success Page

// src/app/checkout/success/page.tsx
import { stripe } from '@/lib/stripe'

export default async function SuccessPage({
  searchParams,
}: {
  searchParams: { session_id: string }
}) {
  const session = await stripe.checkout.sessions.retrieve(
    searchParams.session_id
  )

  return (
    <div className="text-center py-20">
      <h1 className="text-3xl font-bold text-green-600">
        Payment Successful!
      </h1>
      <p className="mt-4">
        Thank you for your purchase. Order ID: {session.id}
      </p>
    </div>
  )
}

Testing with Stripe CLI

# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login and forward webhooks
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# Test card numbers
# Success: 4242 4242 4242 4242
# Decline: 4000 0000 0000 0002

Variations

Subscription Payments

Change mode to subscription:

const checkoutSession = await stripe.checkout.sessions.create({
  mode: 'subscription',
  // ... rest same
})

With Quantity Selector

Pass quantity from frontend and validate:

const quantity = Math.min(Math.max(parseInt(body.quantity) || 1, 1), 99)

Testing Checklist

  • Checkout session creates successfully
  • Redirect to Stripe works
  • Successful payment redirects to success page
  • Webhook receives and processes events
  • Order created in database
  • Cancellation redirects correctly

Want to contribute?

This blueprint is open source. Found an issue or want to improve it? Edit on GitHub