Stripe Checkout and Webhook in a Next.js Application

·16 min read

blog/stripe-checkout-nextjs

Introduction

Stripe is a popular payment processing platform that allows developers to easily integrate secure payment functionality into their websites or applications. In this blog post, we’ll explore how to integrate Stripe into a Next.js application in three different scenarios:

  1. Checkout for a static product and price
  2. Checkout for a dynamic shopping cart

By following this step-by-step guide, you’ll be able to handle payments and process events securely in your Next.js app, regardless of your specific use case.

Setting up a Stripe Account

Before we dive into the implementation, make sure you have a Stripe account set up. If you don’t have one already, go to the Stripe website and sign up for an account. Once you have an account, get your API keys (publishable key and secret key) from the Stripe Dashboard.

Installing Required Dependencies

To get started, you’ll need to install the necessary dependencies in your Next.js project. Open your terminal and navigate to your project’s directory. Run the following command to install the required packages:

npm install stripe @stripe/stripe-js

This command installs the official Stripe library for Node.js and the Stripe.js library for handling Stripe elements in the browser.

1. Integrating Stripe for a Simple Product

For this integration to work we need to:

  1. Create a Stripe Checkout session from our Next.js API route
  2. Redirect the user to Stripe with the session ID created
  3. Handle successful payments by redirecting the user back to our website’s success URL
  4. Handle failed payments by redirecting the user back to our website’s cancel URL

Before diving into the implementation details, let’s take a look at the overall flow of the Stripe Checkout process for a simple product:

Stripe simple flow

As shown in the diagram, the process starts when the user clicks the checkout button, which triggers the creation of a checkout session. The user is then redirected to the Stripe Checkout page. If the payment is successful, the user is redirected to the success URL; otherwise, they are redirected to the cancel URL.

1.1 Creating a Checkout Session

To initiate a Stripe Checkout session, you need to create an API route in your Next.js application. Create a new file named create.ts inside the pages/api/checkout-sessions/ directory.

// pages/api/checkout-sessions/create.ts
import Stripe from "stripe";
import { NextApiRequest, NextApiResponse } from "next";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: "2023-10-16",
});

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  if (req.method === "POST") {
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      line_items: [
        {
          price_data: {
            currency: 'usd',
            product_data: {
              name: 'Your Product Name',
            },
            unit_amount: 1000, // Price in cents
          },
          quantity: 1,
        },
      ],
      mode: 'payment',
      success_url: `${req.headers.origin}/success`,
      cancel_url: `${req.headers.origin}/cancel`,
    });

    res.status(200).json({ id: session.id });
  } else {
    res.setHeader("Allow", "POST");
    res.status(405).end("Method Not Allowed");
  }
}

In this code, we create a new instance of the Stripe client using the secret key. We define a handler function that processes POST requests to the /api/checkout-sessions/create endpoint. Inside the handler, we create a new Checkout session using stripe.checkout.sessions.create().

We specify the payment method types, line items (including the product name, price, and quantity), the mode (set to ‘payment’ for one-time purchases), and the success and cancel URLs to redirect the user after the payment flow.

1.2 Redirecting to the Checkout Page

Now that we have the API route set up, let’s create a button that redirects the user to the Stripe Checkout page. Create a new file named checkout.js inside the pages directory.

// pages/checkout.js
import { loadStripe } from '@stripe/stripe-js';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);

export default function Checkout() {
  const handleCheckout = async () => {
    const stripe = await stripePromise;
    const response = await fetch('/api/checkout-sessions/create', {
      method: 'POST',
    });
    const session = await response.json();
    await stripe.redirectToCheckout({ sessionId: session.id });
  };

  return (
    <div>
      <h1>Stripe Checkout Example</h1>
      <button onClick={handleCheckout}>Checkout</button>
    </div>
  );
}

In this code, we import the loadStripe function from @stripe/stripe-js and initialize it with your Stripe publishable key. We define a handleCheckout function that creates a new Checkout session by making a POST request to the /api/checkout-sessions/create endpoint we created earlier. Once the session is created, we redirect the user to the Stripe Checkout page using stripe.redirectToCheckout().

This is the simplest integration for a static product and price. If you’re selling a digital product, you might want to send the customer an email with a download link after a successful payment. We’ll explore later in this tutorial how to handle post-payment actions using Webhooks. A webhook is a URL that we set up, and Stripe sends a POST request to it with a payload after a payment is processed.

1.3 Handling Successful Payments

When a payment is successful, Stripe will redirect the user back to the success_url specified in the Checkout session. You can create a new page in your Next.js application to handle successful payments.

Create a new file named success.js inside the pages directory.

// pages/success.js
import { useRouter } from 'next/router';

export default function Success() {
  const router = useRouter();
  const { session_id } = router.query;

  return (
    <div>
      <h1>Payment Successful</h1>
      <p>Thank you for your purchase!</p>
      <p>Session ID: {session_id}</p>
    </div>
  );
}

In this code, we use the useRouter hook from Next.js to access the session_id query parameter passed by Stripe. You can use this session_id to retrieve more details about the payment using the Stripe API if needed.

1.4 Handling Failed Payments

If a payment fails or the user cancels the payment, Stripe will redirect the user back to the cancel_url specified in the Checkout session. Create a new file named cancel.js inside the pages directory to handle failed payments.

// pages/cancel.js
export default function Cancel() {
  return (
    <div>
      <h1>Payment Canceled</h1>
      <p>The payment was canceled.</p>
    </div>
  );
}

This page simply displays a message indicating that the payment was canceled.

With these steps, you have successfully integrated Stripe Checkout for a simple product in your Next.js application. The user can click the “Checkout” button, complete the payment on the Stripe Checkout page, and be redirected back to your website based on the payment result.

In the next sections, we’ll explore how to handle dynamic shopping carts using Stripe in a Next.js application.

2. Checkout for a Shopping Cart

In the previous example, you can see that the item in the checkout and its price are hard-coded when creating the checkout session. If we want to use Stripe in an e-commerce shop with many products, we’ll need to send the cart contents to the /api/checkout-sessions/create endpoint.

The flow is the same as the previous one, but now instead of hard-coding the price and product in the create-checkout session, it has to be dynamic, as shown in the diagram below:

Stripe cart flow

Let’s start by creating a checkout page with some hard-cooded products.

2.1 Checkout Page - Shopping Cart

Here’s a simple example of a checkout page with hard-coded products:

// pages/checkout.js
import { loadStripe } from '@stripe/stripe-js';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);

const products = [
  { id: 1, name: 'Product 1', price: 10, image: 'product1.jpg', quantity: 2 },
  { id: 2, name: 'Product 2', price: 20, image: 'product2.jpg', quantity: 1 },
];

export default function Checkout() {
  const handleCheckout = async () => {
    const stripe = await stripePromise;
    const response = await fetch('/api/checkout-sessions/create', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        cartItems: products,
        returnUrl: window.location.origin,
      }),
    });
    const { sessionId } = await response.json();
    await stripe.redirectToCheckout({ sessionId });
  };

  return (
    <div>
      <h1>Checkout</h1>
      {products.map((product) => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>Price: ${product.price}</p>
          <p>Quantity: {product.quantity}</p>
        </div>
      ))}
      <button onClick={handleCheckout}>Proceed to Checkout</button>
    </div>
  );
}

In this example, we have an array of hard-coded products. When the user clicks the “Proceed to Checkout” button, the handleCheckout function sends the products array as cartItems to the /api/checkout-sessions/create endpoint. The endpoint creates a Stripe Checkout session with the provided cart items and returns the sessionId. Finally, we redirect the user to the Stripe Checkout page using stripe.redirectToCheckout().

In the next session I’ll show you how to modify the create session api endpoint to work in a dynamic way with the new payload that we’re sending.

2.2 Create Checkout Session Dynamic

Here’s an example of how creating a checkout session with dynamic content would look like:

// pages/api/checkout-sessions/create.ts
import Stripe from "stripe";
import { NextApiRequest, NextApiResponse } from "next";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: "2023-10-16",
});

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  if (req.method === "POST") {
    const { cartItems, returnUrl } = req.body;

    // Map cart items to the Stripe line_items format
    const line_items = cartItems.map((item) => {
      return {
        price_data: {
          currency: "usd",
          product_data: {
            name: item.name,
            images: [item.image],
          },
          unit_amount: item.price * 100, // TODO: Price should be retrieved from db
        },
        quantity: item.quantity,
      };
    });

    const session = await stripe.checkout.sessions.create({
      payment_method_types: ["card"],
      line_items,
      mode: "payment",
      success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${returnUrl}`,
    });

    res.status(200).json({ sessionId: session.id });
  } else {
    res.setHeader("Allow", "POST");
    res.status(405).end("Method Not Allowed");
  }
}

In this code, we receive the cartItems and returnUrl from the request body. We map the cart items to the Stripe line_items format, including the product name, image, price, and quantity. Then, we create a new Checkout session using stripe.checkout.sessions.create(), passing the line_items, mode, success_url, and cancel_url.

Note that we include the {CHECKOUT_SESSION_ID} placeholder in the success_url, which will be replaced with the actual session ID. This allows you to retrieve the session on the success page and show a summary of the order.

Also, an important detail to keep in mind is that in the example, for simplicity, I’m sending the price that I’m getting from the API. However, in reality, we should use the product ID and retrieve the price from our backend function. Otherwise, a user could intercept the request and set the price to 0.

With the checkout page and the create-session functionality, we can already redirect users to the Stripe Checkout page. Next, we need to handle successful and failed payments. As you can see in the code, I normally use the practice of setting the cancel/failure URL to be the cart checkout page. We could show a toast message, but I think it’s a very good pattern and simplifies things. In the next section, I’ll show you how to build a success page with an order summary.

2.3 Success Page

After a successful payment, you can display a success page to the user. Here’s an example of a success page with an order summary:

// pages/success.js
import { useEffect } from 'react';
import { GetServerSideProps } from 'next';
import Stripe from 'stripe';
import { useCart } from '@/hooks/shoppingCartProvider';
import OrderSummary from '@/components/OrderSummary';

type Props = {
  stripeSession: Stripe.Checkout.Session;
};

const Success: React.FC<Props> = ({ stripeSession }) => {
  const { clearCart } = useCart();

  useEffect(() => {
    clearCart();
  }, [clearCart]);

  return (
    <div>
      <h1>Payment Completed</h1>
      <OrderSummary stripeSession={stripeSession} />
    </div>
  );
};

export default Success;

export const getServerSideProps: GetServerSideProps = async (context) => {
  const stripeSecret = getEnv('STRIPE_SECRET_KEY');
  const stripe = new Stripe(stripeSecret, {
    apiVersion: '2023-10-16',
  });

  const sessionId = context.query.session_id;

  if (typeof sessionId !== 'string') {
    return {
      notFound: true,
    };
  }

  try {
    const session = await stripe.checkout.sessions.retrieve(sessionId, {
      expand: ['line_items.data.price.product'],
    });

    return {
      props: {
        stripeSession: session,
      },
    };
  } catch (error) {
    console.error(error);
    return {
      notFound: true,
    };
  }
};

In this success page, we use getServerSideProps to retrieve the Stripe Checkout session using the session_id query parameter. We pass the session data as props to the Success component.

Inside the Success component, we clear the cart using the clearCart function from the useCart hook. This ensures that the cart is emptied after a successful payment.

Finally, we display a success message and render the OrderSummary component, passing the stripeSession data as a prop. The OrderSummary component can display the order details based on the Stripe session data.

Here’s an example of the OrderSummary component:

// components/OrderSummary.js
import { useState } from 'react';

interface Props {
  session: Stripe.Checkout.Session;
}

const OrderSummary: React.FC<Props> = ({ session }) => {
  const formatAddress = (address: Stripe.Address) => {
    const { line1, line2, city, state, postal_code, country } = address;
    const formattedAddress = [line1, line2, city, state, postal_code, country]
      .filter(Boolean)
      .join(', ');
    return formattedAddress || 'N/A';
  };

  return (
    <div>
      <h2>Order Summary</h2>
      <p>Order ID: {session.id}</p>
      <p>Total Amount: ${session.amount_total / 100}</p>
      <p>Shipping Address: {formatAddress(session.shipping.address)}</p>
      <div>
        <h3>Line Items</h3>
        {session.line_items.data.map((item) => (
          <div key={item.id}>
            <p>{item.description}</p>
            <p>Quantity: {item.quantity}</p>
            <p>Price: ${item.amount_total / 100}</p>
          </div>
        ))}
      </div>
    </div>
  );
};

export default OrderSummary;

In this simplified OrderSummary component, we display the order ID, total amount, and shipping address. We also provide a toggle button to show or hide the line item details.

With the checkout page, dynamic create-session functionality, and success page with order summary, you have a complete example of a shopping cart checkout using Stripe in a Next.js application.

Remember to handle edge cases, error scenarios, and provide a user-friendly experience throughout the checkout process.

3. Handling Stripe Events with Webhooks

Webhooks play a crucial role in handling events from Stripe, such as successful payments, subscription updates, or failed transactions. They allow Stripe to send real-time notifications to your application when specific events occur. In this section, we’ll explore how to set up a webhook endpoint in your Next.js application to handle Stripe events.

The diagram below depicts the flow of handling Stripe events using webhooks:

Stripe webhook flow

3.1 Creating a Webhook Endpoint

To create a webhook endpoint, let’s create a new file named stripe.ts inside the pages/api/webhooks directory.

// pages/api/webhook/stripe.ts
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

export default async function handler(req, res) {
  if (req.method === 'POST') {
    const sig = req.headers['stripe-signature'];
    let event;

    try {
      event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
    } catch (err) {
      console.error('Error verifying webhook signature:', err);
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    // Handle the event
    switch (event.type) {
      case 'checkout.session.completed':
        // Handle successful payment
        break;
      case 'invoice.payment_succeeded':
        // Handle successful subscription payment
        break;
      // Add more cases for other event types you want to handle
      default:
        console.log(`Unhandled event type: ${event.type}`);
    }

    res.status(200).json({ received: true });
  } else {
    res.setHeader('Allow', 'POST');
    res.status(405).end('Method Not Allowed');
  }
}

export const config = {
  api: {
    bodyParser: false,
  },
};

In this code, we create a new instance of the Stripe client using the secret key. We define a handler function that processes POST requests to the /api/webhook/stripe endpoint. Inside the handler, we verify the webhook signature using stripe.webhooks.constructEvent() to ensure the event is coming from Stripe.

Once the event is verified, we use a switch statement to handle different event types. In this example, we handle the checkout.session.completed event for successful payments and the invoice.payment_succeeded event for successful subscription payments. You can add more cases to handle other event types based on your application’s requirements.

Finally, we disable the default body parser using export const config to ensure the raw request body is available for signature verification.

3.2 Configuring Webhook in Stripe Dashboard

To start receiving webhook events, you need to configure the webhook endpoint in the Stripe Dashboard. Follow these steps:

  1. Go to the Stripe Dashboard and navigate to the “Developers” section.
  2. Click on “Webhooks” in the left sidebar.
  3. Click on the “Add endpoint” button.
  4. Enter your webhook URL (e.g., https://your-domain.com/api/webhook/stripe) and select the events you want to receive.
  5. Click on “Add endpoint” to save the configuration.

Make sure to replace your-domain.com with your actual domain.

3.3 Testing Webhooks

Testing webhooks locally can be easily done using the Stripe CLI. Here’s a simplified guide:

  1. Install the Stripe CLI by following the instructions in the Stripe CLI documentation.
  2. Login to your Stripe account using the CLI:
stripe login
  1. Start the webhook forwarding process:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
  1. The CLI will display a webhook signing secret. Copy this secret and set it as the STRIPE_WEBHOOK_SECRET environment variable in your Next.js application.
  2. Trigger test events using the Stripe Dashboard or the CLI. For example, to trigger a checkout.session.completed event:
stripe trigger checkout.session.completed

Your local webhook endpoint will receive the test event, and you can debug and test your webhook handling logic.

3.4 Handling Webhook Events

When a webhook event is received, you can perform various actions based on the event type. Here are a few examples:

  • For the checkout.session.completed event, you can retrieve the session details and create a new order in your database, send an order confirmation email, or update inventory.
  • For the invoice.payment_succeeded event, you can update the subscription status in your database, grant access to premium features, or notify the user about the successful payment.
  • For the customer.subscription.deleted event, you can revoke access to premium features, update the user’s subscription status, or send a cancellation confirmation email.

The specific actions you take will depend on your application’s requirements and business logic.

3.5 Conclusion

Webhooks are essential for handling Stripe events in your Next.js application. By setting up a webhook endpoint, configuring it in the Stripe Dashboard, and handling different event types, you can keep your application in sync with Stripe and perform necessary actions based on payment-related events.

Remember to keep your webhook signing secret secure and never expose it publicly. Always use environment variables to store sensitive information.

Stripe provides a wide range of event types, so make sure to explore the Stripe documentation to learn more about handling different scenarios and customizing your webhook logic to suit your application’s needs.

With webhooks set up, you can now confidently handle Stripe events and ensure a smooth payment experience for your users.

Recap

In this guide, we covered the essential steps to integrate Stripe payments into a Next.js application. From setting up checkout sessions for static products and dynamic shopping carts to webhooks, you now have the knowledge to implement secure and seamless payments in your Next.js projects.

Remember to keep your Stripe secrets secure and refer to the Stripe documentation for more advanced features. Happy coding and successful payments!

Related PostsNextjs, Development, Stripe

Profile

I'm a software developer and consultant. I help companies build great products. Contact me by email.

Get my new content delivered straight to your inbox. No spam, ever.