Skip to main content
Back to Blog
Tutorials
3 min read
November 20, 2024

How to Set Up Stripe Payments in a Next.js Application

Integrate Stripe payments into your Next.js application with Checkout Sessions, webhooks, and subscription management. Production-ready implementation.

Ryel Banfield

Founder & Lead Developer

Stripe is the standard for online payments. This tutorial covers integrating Stripe Checkout Sessions into a Next.js App Router application for one-time payments and subscriptions.

Prerequisites

  • Next.js 14+ with App Router
  • A Stripe account (test mode for development)
  • Node.js 18+

Step 1: Install Dependencies

pnpm add stripe @stripe/stripe-js
  • stripe: Server-side Stripe SDK
  • @stripe/stripe-js: Client-side Stripe loader

Step 2: Configure Environment Variables

# .env.local
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

Get these from your Stripe Dashboard > Developers > API Keys.

Step 3: Create the Stripe Instance

// lib/stripe.ts
import Stripe from "stripe";

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2024-12-18.acacia",
  typescript: true,
});

Step 4: Create a Checkout Session API Route

// app/api/checkout/route.ts
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";

export async function POST(request: Request) {
  try {
    const { priceId, mode } = await request.json();

    const session = await stripe.checkout.sessions.create({
      mode: mode || "payment", // "payment" for one-time, "subscription" for recurring
      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`,
    });

    return NextResponse.json({ url: session.url });
  } catch (error) {
    console.error("Stripe checkout error:", error);
    return NextResponse.json(
      { error: "Failed to create checkout session" },
      { status: 500 }
    );
  }
}

Step 5: Create a Checkout Button Component

// components/CheckoutButton.tsx
"use client";

import { useState } from "react";

interface CheckoutButtonProps {
  priceId: string;
  mode?: "payment" | "subscription";
  label?: string;
}

export function CheckoutButton({ priceId, mode = "payment", label = "Buy Now" }: CheckoutButtonProps) {
  const [loading, setLoading] = useState(false);

  async function handleCheckout() {
    setLoading(true);

    try {
      const response = await fetch("/api/checkout", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ priceId, mode }),
      });

      const { url } = await response.json();

      if (url) {
        window.location.href = url;
      }
    } catch (error) {
      console.error("Checkout error:", error);
    } finally {
      setLoading(false);
    }
  }

  return (
    <button
      onClick={handleCheckout}
      disabled={loading}
      className="rounded-md bg-blue-600 px-6 py-3 text-white hover:bg-blue-700 disabled:opacity-50"
    >
      {loading ? "Redirecting..." : label}
    </button>
  );
}

Step 6: Handle Webhooks

Webhooks notify your server when payment events occur (successful payments, failed charges, subscription changes):

// app/api/webhooks/stripe/route.ts
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
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) {
    console.error("Webhook signature verification failed:", error);
    return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
  }

  switch (event.type) {
    case "checkout.session.completed": {
      const session = event.data.object as Stripe.Checkout.Session;
      // Fulfill the order
      // - Update database
      // - Send confirmation email
      // - Grant access
      console.log("Payment successful:", session.id);
      break;
    }

    case "customer.subscription.updated": {
      const subscription = event.data.object as Stripe.Subscription;
      // Update subscription status in your database
      console.log("Subscription updated:", subscription.id);
      break;
    }

    case "customer.subscription.deleted": {
      const subscription = event.data.object as Stripe.Subscription;
      // Revoke access
      console.log("Subscription cancelled:", subscription.id);
      break;
    }

    case "invoice.payment_failed": {
      const invoice = event.data.object as Stripe.Invoice;
      // Notify customer of failed payment
      console.log("Payment failed:", invoice.id);
      break;
    }

    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  return NextResponse.json({ received: true });
}

Important: The webhook route must receive the raw body (not parsed JSON) for signature verification. Next.js App Router handles this correctly when you use request.text().

Step 7: Create Products and Prices in Stripe

Use the Stripe Dashboard or API:

// One-time payment product
const product = await stripe.products.create({
  name: "Website Audit",
  description: "Comprehensive website performance and SEO audit",
});

const price = await stripe.prices.create({
  product: product.id,
  unit_amount: 49900, // $499.00
  currency: "usd",
});

// Subscription product
const subProduct = await stripe.products.create({
  name: "Monthly Maintenance",
});

const subPrice = await stripe.prices.create({
  product: subProduct.id,
  unit_amount: 9900, // $99.00/month
  currency: "usd",
  recurring: { interval: "month" },
});

Step 8: Success Page

// app/success/page.tsx
import { stripe } from "@/lib/stripe";

interface Props {
  searchParams: Promise<{ session_id?: string }>;
}

export default async function SuccessPage({ searchParams }: Props) {
  const { session_id } = await searchParams;

  if (!session_id) {
    return <p>No session found.</p>;
  }

  const session = await stripe.checkout.sessions.retrieve(session_id);

  return (
    <main className="flex min-h-screen items-center justify-center">
      <div className="text-center">
        <h1 className="text-3xl font-bold">Payment Successful</h1>
        <p className="mt-4 text-gray-600">
          Thank you for your purchase. A confirmation email has been sent to{" "}
          {session.customer_details?.email}.
        </p>
      </div>
    </main>
  );
}

Step 9: Test with Stripe CLI

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

# Login
stripe login

# Forward webhooks to your local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# Trigger a test event
stripe trigger checkout.session.completed

Test Card Numbers

Card NumberResult
4242 4242 4242 4242Success
4000 0000 0000 0002Declined
4000 0000 0000 32203D Secure required

Use any future expiry date and any 3-digit CVC.

Security Checklist

  • Secret key never exposed to client
  • Webhook signature verified
  • HTTPS enforced (Vercel does this automatically)
  • Idempotent webhook handling
  • Amount validated on server side
  • Rate limiting on checkout endpoint

Need Payment Integration?

We integrate Stripe, subscription billing, and custom checkout flows for e-commerce and SaaS applications. Contact us for payment integration services.

StripepaymentsNext.jse-commercetutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles