← Back to posts

How to Setup Stripe Webhook for Subscriptions in NextJS

Published: 8/3/2024

The first thing that needs to be done when setting up stripe webhooks is to enter test mode and go to the stripe dashboard, as seen below:

An Image

In your NextJS project you need to create a file called “env.local” in the root directory of the project, if it hasn’t already been created. In this file you will need to add the following properties.

STRIPE_KEY=<STRIPE_PUBLISHABLE_KEY>
STRIPE_SECRET=<STRIPE_SECRET>

You will need to change the <STRIPE_PUBLISHABLE_KEY> with the publishable key found on the test dashboard which is obfuscated by a line in the above image. Also change the <STRIPE_SECRET> with the secret key which needs to be revealed before being copied.

The next step is to go to the webhook page in stripe whilst still in test mode, which should look like the image below:

An Image

Click on “Add local Listener” and you should be directed to a page that looks similar to the one shown below:

An Image

Do the first step of downloading the CLI and logging in with stripe. Then you need to copy the webhook secret which is labelled as the “endpointSecret” variable. Copy this and put it into the previously created “env.local” file under the previous text like so:

STRIPE_WEBHOOK_SECRET=<WEBHOOK_SECRET>

Make sure to replace <WEBHOOK_SECRET> with the webhook secret. After this you need to enter the following command into a terminal/command prompt:

stripe listen --forward-to localhost:3000/api/webhook/stripe

The next step is to create the file for the route. As we are doing this in the app router, the path of the file should be /app/api/webhook/stripe/route.ts.

The initial contents of the newly created route.ts file should be as follows:

import { NextResponse } from "next/server";
import Stripe from "stripe";

/**
 * Handles the POST request for the Stripe webhook endpoint.
 * 
 * @param req - The incoming request object.
 * @returns A Promise that resolves to a NextResponse object.
 */
export async function POST(req: Request): Promise<NextResponse> {
    // Retrieve the Stripe-Signature header
    const sig = req.headers.get("Stripe-Signature");
    if (sig == null) {
        return NextResponse.json({ error: "No signature" }, { status: 400 });
    }

    // Retrieve the Stripe webhook secret
    const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
    if (endpointSecret == null) {
        return NextResponse.json({ error: "Stripe webhook secret not set" }, { status: 400 });
    }

    // Retrieve the request body
    const body = await req.text();

    // Retrieve the Stripe secret
    const secret = process.env.STRIPE_SECRET;
    if (secret == null) {
        return NextResponse.json({ error: "Stripe secret not set" }, { status: 400 });
    }

    // Create a new Stripe object
    const stripe = new Stripe(secret);

    let event;

    // Verify the Stripe event signature
    try {
        event = stripe.webhooks.constructEvent(body, sig, endpointSecret);
    } catch (err: any) {
        console.error(`Webhook signature verification failed. ${err.message}`);
        return NextResponse.json({ error: err.message }, { status: 400 });
    }

    let result = false;
    
    // Handle different types of Stripe events
    switch (event.type) {
      case "customer.subscription.created":
        break;
      case "customer.subscription.deleted":
        break;
      case "customer.subscription.updated":
        break;
      case "invoice.payment_succeeded":
        break;
      default:
        break;
    }
    // Just respond 200 if unhandled event type
    return new NextResponse("ok");
}

The comments should explain what is happening here. The next step is to add in the code to handle the events and return responses depending on whether it runs successfully or unsuccessfully. Every system is different but you may be asking why there is an invoice.payment_succeeded event, the reason is that this event happens every month as well as on an initial payment for the subscription and using the subscription reason you can figure out if it is a recurring payment and use that to update things such as limits that need to be updated every month.

As every system is implemented differently they all require different code which must be decided by you but some basic variables that may be needed in each section are put as code sections below:

case "customer.subscription.created":  
  const subscription = event.data.object;  
  // We assume that the customer exists as the subscription was created  
  const customer = await stripe.customers.retrieve(subscription.customer as string) as Stripe.Customer;  
  const priceID = subscription.items.data[0].price.id;  
  // You can use the priceID to get the plan
  // You could use a switch statement or a call to yourdatabase  
  // This is an example of a successful output from the switch case    
  return NextResponse.json({ message: "ok" }, { status: 200 });  
  break;
case "customer.subscription.deleted":  
  const subscription: Stripe.Subscription = event.data.object;  
  const customer: Stripe.Customer = await stripe.customers.retrieve(subscription.customer.toString()) as Stripe.Customer;  
  // You can use the subscription id to delete it
  // Or use the customer email to delete the subscription from your database  
  break;
case "customer.subscription.updated":  
  // Updating a subscription is similar to creating one
  // Except that you can use the customer email or subscription id to update it  
  // The update subscription is called if it is changed
  // And NOT if a recurring payment occurs so is not called every subscription period  
  const subscription = event.data.object;  
  // We assume that the customer exists as the subscription was created  
  const customer = await stripe.customers.retrieve(subscription.customer as string) as Stripe.Customer;  
  const priceID = subscription.items.data[0].price.id;  
  break;
case "invoice.payment_succeeded":  
  // Checking if a recurring subscription payment  
  if(event.data.object.billing_reason == "subscription_cycle") {  
    const customerID = event.data.object.customer?.toString();  
    const customer = await stripe.customers.retrieve(customerID??"") as Stripe.Customer;  
    // Things such as limits can be changed here
    // Possibly using the customer email to identify the customer or their stripe id  
  }  
  // There is no else statement here
  // As we know that all types of payment succeeded are sent here
  // So we expect some aren't the right type
  // And don't want stripe to keep retrying  
  break;

Good luck in implementing the solution for your codebase, hope this helped with the implementation for NextJS.