November 20, 2024

How to Integrate Stripe Payment in Next.js 15: Step-by-Step Guide

Learn how to integrate Stripe payments in your Next.js 15 application with this step-by-step guide. Perfect for SaaS and digital products.

How to Integrate Stripe Payment in Next.js 15: Step-by-Step Guide

If you're short on time, you can find the full code on GitHub.

Introduction

For a long time, I thought managing payments was scary. It's one of the most critical parts of an app that you really don't want to mess up. But nowadays, it's much simpler. In this tutorial, we'll use Stripe to set up payments in Next.js 15 with the app router.

Here's what we'll cover:

  • How to create a Stripe product and price
  • How to create a payment button in your Next.js app
  • How to set up a webhook to handle Stripe events and trigger actions in your Next.js app

By the end of this guide, you'll have a working payment integration ready to go.

Explanation of the Payment Workflow

Here's how the payment process works step by step:

  1. The user clicks on the Pay button in your app.
  2. Your app creates a Stripe Checkout session.
  3. The user is redirected to the Stripe-hosted checkout page.
  4. The user completes the payment.
  5. Stripe triggers specific events once the payment is processed.
  6. Your app listens for these events through a webhook and executes code based on the event type. In this guide, we'll keep it simple and just display a message after payment.

Here's a visual representation of the workflow:

Copy the Price ID in Stripe Dashboard

Step 1: Create Your Next.js Project

The first step is to set up a new Next.js 15 project. Open your terminal and run the following command:

npx create-next-app@latest

You'll be prompted to enter a name for your project. Type in a name, and for the rest of the prompts, you can simply press Enter to select the default options.

Installation process in Next.js 15

That's it! You now have a fresh Next.js project ready to go.

Step 2: Create a Price ID in Stripe Dashboard

Now, we'll set up a product and get its Price ID from Stripe. Follow these steps:

  1. Log in to your Stripe dashboard and toggle Test Mode in the top bar. This helps you avoid making mistakes while experimenting.
  2. In the left sidebar, click Product Catalogue and then click Create Product.
  3. Fill out the form with the details of your product. For this tutorial, we'll use a one-time payment option as an example.
Add a new product in Stripe Dashboard
  1. Click Add Product. Your product will now appear in the list.
  2. Select your product from the list.
  3. In the Pricing section, click on the price to open the price details page.
  4. On the price details page, look for the Price ID in the top-right corner and click to copy it.
Copy the Price ID in Stripe Dashboard

You'll need this Price ID in the next step, so keep it handy.

Step 3: Set Up Stripe in Next.js

With your product and Price ID ready, let's integrate Stripe into your Next.js project.

First, navigate back to your project directory. Install the necessary dependencies for using Stripe's API by running:

pnpm add stripe @stripe/stripe-js

Next, create a .env.local file in the root of your project to store environment variables. Add the following variables:

BASE_URL=http://localhost:3000STRIPE_PRICE_ID=NEXT_PUBLIC_STRIPE_PUBLIC_KEY=STRIPE_SECRET_KEY=
  • BASE_URL: Your development URL, which will be useful later.
  • STRIPE_PRICE_ID: Paste the Price ID you copied in the previous step.
  • NEXT_PUBLIC_STRIPE_PUBLIC_KEY and STRIPE_SECRET_KEY: To get these keys, go back to your Stripe dashboard (still in Test Mode).
    1. Click Developers in the top bar and select the API Keys tab.
    2. Copy the Publishable Key and paste it into NEXT_PUBLIC_STRIPE_PUBLIC_KEY. It should look like pk_test_xxxxx.
    3. Click Reveal Secret Key to view your secret key. Copy it and paste it into STRIPE_SECRET_KEY. It should look like sk_test_xxxxx.

Now, set up Stripe in your code. In your app directory, create a lib folder. Inside, create two files: stripe-server.ts and stripe-client.ts.

In stripe-server.ts, add the following code:

import Stripe from "stripe";export const stripeApi = new Stripe(process.env.STRIPE_SECRET_KEY!);

This initializes the Stripe API on the server side, allowing you to interact with it in your application.

In stripe-client.ts, add:

import { loadStripe, Stripe } from "@stripe/stripe-js";let stripePromise: Promise<Stripe | null>;export const getStripe = () => {  if (!process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY) {    throw new Error("Missing Stripe Public Key");  }  if (!stripePromise) {    stripePromise = loadStripe(      process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY as string,    );  }  return stripePromise;};

This initializes the Stripe library on the client side. The getStripe function ensures that only one instance of the client library is created.

Launch your SaaS without giving up your evenings

Ready.js is a code boilerplate that gives you everything you need to launch quickly and start generating income alongside your main job.

At this point, your project should have the necessary setup to start using Stripe. Your folder structure should look something like this:

Next.js 15 with Stripe integration folder structure

Step 4: Create the Purchase Button

Now let's add a purchase button to your app that initializes a Stripe Checkout session.

Create a Server Action

Start by creating a components folder in your app directory. Inside, create a purchase-button folder. In this folder, create a purchase.ts file and add the following code:

"use server";import { stripeApi } from "@/app/lib/stripe-server";export default async function purchase() {  try {    const priceId = process.env.STRIPE_PRICE_ID;    const stripeCheckoutSession = await stripeApi.checkout.sessions.create({      payment_method_types: ["card"],      line_items: [        {          price: priceId,          quantity: 1,        },      ],      mode: "payment",      success_url: `${process.env.BASE_URL}/?checkout=success`,      cancel_url: `${process.env.BASE_URL}/?checkout=cancelled`,      metadata: {        // Add any metadata you'd like to store for this session      },    });    return { checkoutSessionId: stripeCheckoutSession.id };  } catch (e) {    console.error(e);    return { checkoutSessionId: null };  }}

What's happening here?

  • The purchase function is a server action (indicated by "use server" at the top). Server actions run securely on the server.
  • The function creates a Stripe Checkout session using the stripeApi instance and the price ID from your environment variables.
  • It sets up options like payment_method_types (in this case, card payments) and defines the mode as payment for a one-time payment.
  • The success_url and cancel_url define where the user is redirected after completing or canceling the payment.
  • Finally, the function returns the checkoutSessionId. If something goes wrong, it logs the error and returns null.

Create the Frontend Button

Now, let's add the frontend logic. In the same purchase-button folder, create a purchase-button.tsx file and add the following code:

"use client";import { getStripe } from "@/app/lib/stripe-client";import { useState } from "react";import purchase from "./purchase";export const PurchaseButton = () => {  const [isLoading, setIsLoading] = useState(false);  const onSubmit = async () => {    setIsLoading(true);    const res = await purchase();    if (!res.checkoutSessionId) {      console.error("Failed to create Stripe Checkout session.");      setIsLoading(false);      return;    }    const stripe = await getStripe();    await stripe?.redirectToCheckout({      sessionId: res.checkoutSessionId,    });    setIsLoading(false);  };  return (    <button      onClick={onSubmit}      disabled={isLoading}      className="bg-blue-500 px-4 py-2 text-white"    >      {isLoading ? "Loading..." : "Purchase"}    </button>  );};

What's happening here?

  • This is a simple React component that displays a button.
  • When the button is clicked, the onSubmit function calls the purchase server action to create a Stripe Checkout session.
  • If the session is created successfully, the user is redirected to the Stripe Checkout page using the Stripe client library.
  • A loading state ensures the button is disabled while the process is running, giving users clear feedback.

Add the Button to the Homepage

Now, let's test the button. Replace everything in your homepage with the following code:

import { PurchaseButton } from "./components/purchase-button/purchase-button";export default async function Home({  searchParams,}: {  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;}) {  const { checkout } = await searchParams;  return (    <main className="flex h-screen w-screen items-center justify-center">      <div className="space-y-4">        {checkout === "success" ? (          <p className="text-green-500">Payment successful!</p>        ) : (          <p className="text-red-500">Payment cancelled.</p>        )}        <PurchaseButton />      </div>    </main>  );}

Here, we simply add the PurchaseButton component to the homepage and display a message to inform the user whether the checkout was successful or cancelled.

Now, start your development server:

pnpm dev

Navigate to http://localhost:3000. You should see your button. Click on it to test the flow:

  1. You'll be redirected to the Stripe Checkout page.
  2. Enter the following test payment details:
    • Card number: 4242 4242 4242 4242
    • Expiry date: 12/34
    • CVC: 567
    • The cardholder name can be anything.
  3. Complete the payment.

Once the payment is successful, you'll be redirected to your homepage with ?checkout=success added to the URL, and you should see the success message.

If you cancel, the URL will show ?checkout=cancelled.

Step 5: Create the Webhook to Handle Actions After Purchase

Great, your users can now pay you—that's already a big step forward! But in most real-world applications, like a SaaS, you'll need to perform actions after a purchase, such as updating a user's subscription status or recording the payment in your database. This is where webhooks come in.

A webhook is essentially an API endpoint that Stripe calls when specific events occur, like a payment being completed. Let's set one up.

Simulate Webhooks Locally with Stripe CLI

To test webhooks locally, we'll use Stripe CLI. Follow these steps to set it up:

  1. Install Stripe CLI. For macOS users, you can install it using Homebrew:

    brew install stripe/stripe-cli/stripe

    For other installation methods, check Stripe's documentation.

  2. Log in to your Stripe account using the CLI:

    stripe login
  3. Start the webhook listener:

    stripe listen --forward-to localhost:3000/api/webhooks/checkout

    This command creates a listener that forwards Stripe events to your local API. When it starts, it will provide a Webhook Secret, which looks like whsec_xxxxx. Copy this secret and add it to your .env.local file as STRIPE_WEBHOOK_SIGNING_SECRET.

Note: Keep the terminal with this command running, as closing it will stop the listener.

Implement the API Route

Now, let's create the webhook API route in your Next.js app:

  1. In your app directory, create a new folder structure: api/webhooks/checkout.
  2. Inside the checkout folder, create a route.ts file.
  3. Add the following code:
import { NextRequest } from "next/server";import { default as stripe, default as Stripe } from "stripe";export async function POST(request: NextRequest) {  const body = await request.text();  const headers = await request.headers;  if (!process.env.STRIPE_WEBHOOK_SIGNING_SECRET) {    return new Response("Missing stripe webhook signing secret", {      status: 500,    });  }  const webhookSigningSecret = process.env.STRIPE_WEBHOOK_SIGNING_SECRET!;  const stripeSignature = headers.get("stripe-signature");  if (!stripeSignature) {    return new Response("Missing stripe signature", { status: 400 });  }  let event: Stripe.Event;  try {    event = stripe.webhooks.constructEvent(      body,      stripeSignature,      webhookSigningSecret,    );  } catch (err) {    console.error("Webhook signature verification failed:", err);    return new Response(      `Webhook Error: ${err instanceof Error ? err.message : "Unknown error"}`,      {        status: 400,      },    );  }  if (event.type !== "checkout.session.completed") {    return new Response(`Unhandled event type: ${event.type}`, { status: 400 });  }  const checkoutSession = event.data.object as Stripe.Checkout.Session;  try {    const userId = checkoutSession.metadata?.userId;    const userEmail = checkoutSession.customer_details?.email;    // Perform any required actions, such as updating the database    // For instance:    // await prisma.user.update({    //   where: { id: userId },    //   data: { subscriptionStatus: "ACTIVE" },    // });    console.log(`Payment processed for user ${userId} (${userEmail})`);    return new Response("Payment processed successfully", { status: 200 });  } catch (error) {    console.error("Payment processing failed:", error);    return new Response("Internal server error", { status: 500 });  }}

What's Happening Here?

  1. Signature Validation
    • The webhook extracts the raw request body and headers.
    • It uses the stripe-signature header and Stripe's SDK to validate the webhook's authenticity. If the signature is invalid or missing, an error is returned.
  2. Event Handling
    • We filter for checkout.session.completed events, which are triggered when a payment is successfully completed.
    • You can add more event types here if needed.
  3. Database Update (or Other Actions)
    • You can extract user-specific data, such as the userId or email, from the Checkout.Session object.
    • Use this data to perform actions, like activating a user's subscription or logging the payment.
  4. Response
    • The webhook logs the processed event and returns an appropriate response.

Test Your Webhook

Make sure your Stripe webhook listener is still running. Then, go back to your homepage and click the purchase button.

  1. Enter the test payment details:
    • Card number: 4242 4242 4242 4242
    • Expiry date: 12/34
    • CVC: 567
  2. Complete the payment.

Check your terminal for the webhook listener. You should see a log indicating that the payment was processed, along with details about the user.

Stripe webhook listener logs on checkout success

And that's it! You now have a functioning webhook to handle Stripe events in your Next.js app.

Going into Production

Now that your Stripe integration is working in development, it's time to prepare for production. This involves updating your environment variables and setting up a webhook for your live environment.

Set Up Environment Variables

To switch to production, follow these steps:

  1. Update Your BASE_URL
    • Replace your development URL in BASE_URL with your production URL, e.g., https://yourdomain.com.
  2. Switch to Live Mode in Stripe
    • Log in to your Stripe dashboard and toggle off Test Mode in the top bar.
  3. Create a New Product
    • In the Product Catalogue, create a new product and price just like you did for testing.
    • Copy the new Price ID and update the STRIPE_PRICE_ID in your environment variables.
  4. Update the API Keys
    • Go to the Developers section in the Stripe dashboard and open the API Keys tab.
    • Copy the Publishable Key (it should look like pk_live_xxxx) and paste it into NEXT_PUBLIC_STRIPE_PUBLIC_KEY.
    • Create a new Secret Key:
      • Select "Building your own integration" and click "Create Secret Key".
      • Give the key a name, then copy it (it should look like sk_live_xxxx) and paste it into STRIPE_SECRET_KEY.

Your .env file should now look something like this:

BASE_URL=https://yourdomain.comSTRIPE_PRICE_ID=price_live_xxxxNEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_live_xxxxSTRIPE_SECRET_KEY=sk_live_xxxx

Create the Webhook for Production

To handle Stripe events in production, you'll need to set up a live webhook:

  1. Add a New Webhook Endpoint
    • In your Stripe dashboard, go to the Developers section and select the Event Destinations tab.
    • Click Add Endpoint.
  2. Configure the Endpoint
    • Enter the URL of your live webhook endpoint, e.g., https://yourdomain.com/api/webhooks/checkout.
    • Select the events to listen to. For this guide, choose checkout.session.completed.
    • If your app needs to handle additional events, you can select them here.
  3. Save and Copy the Webhook Secret
    • Once the endpoint is created, it will appear in the list.
    • Click Reveal under the Signing Secret column to view your Webhook Secret (it should look like whsec_xxxxx).
    • Add this secret to your production environment variables as STRIPE_WEBHOOK_SIGNING_SECRET.

Final Steps

Deploy your Next.js app to your production environment and ensure all your environment variables are set. With these updates, your app is ready to handle live payments and webhook events.

Conclusion

That's it! You now have everything you need to accept payments in your Next.js 15 application using Stripe. With this setup, you can confidently build your SaaS or any other product that requires payment integration.

Want to launch your SaaS faster?

Ready.js saves you 40+ hours of boilerplate work, so you can focus on building your product. It includes all the essential features like Stripe integration, authentication, blogging capabilities, and more.
MC

Manuel Coffin

Author

Outdoor sports enthusiast, frontend geek and Creator of Ready.js Boilerplate