Skip to content

How to Create a Webhook Endpoint to Receive Stripe Events in Next.js

Published: at 08:29 AM

In the previous post, we set up Stripe in our project. In this post, we’ll create a webhook to update payment information in the database after the user completes the payment.

We’ll start by defining our webhook endpoint on the Stripe website. First, create a file in src/app/api/webhooks/route.ts. After that, open the webhooks tab on the Stripe website.

webhook-tab-image

Replace the endpoint with your base URL and add the event checkout.session.completed.

webhook-configuration-image

Next, we need logic to update the information after the user completes the payment. To do this, we should catch the event from the Stripe checkout session. We will receive the stripe-signature from the headers and verify it. Here is the logic for the webhook file. You can also add conditions for several events such as subscription schedules, coupons, etc., using if (event.type === YOUR_SELECTED_EVENT).

// file: src/app/api/webhooks/route.ts

import { db } from "@/db";
import { stripe } from "@/lib/stripe";
import { headers } from "next/headers";
import { NextResponse } from "next/server";
import Stripe from "stripe";

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

    if (!signature) {
      return new Response("Invalid signature", { status: 400 });
    }

    const event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );

    if (event.type === "checkout.session.completed") {
      if (!event.data.object.customer_details?.email) {
        throw new Error("Missing user email");
      }

      const session = event.data.object as Stripe.Checkout.Session;

      const { userId, orderId } = session.metadata || {
        userId: null,
        orderId: null,
      };

      if (!userId || !orderId) {
        throw new Error("Invalid request metadata");
      }

      const billingAddress = session.customer_details!.address;
      const shippingAddress = session.shipping_details!.address;

      await db.order.update({
        where: {
          id: orderId,
        },
        data: {
          isPaid: true,
          shippingAddress: {
            create: {
              name: session.customer_details!.name!,
              city: shippingAddress!.city!,
              country: shippingAddress!.country!,
              postalCode: shippingAddress!.postal_code!,
              street: shippingAddress!.line1!,
              state: shippingAddress!.state!,
            },
          },
          billingAddress: {
            create: {
              name: session.customer_details!.name!,
              city: billingAddress!.city!,
              country: billingAddress!.country!,
              postalCode: billingAddress!.postal_code!,
              street: billingAddress!.line1!,
              state: billingAddress!.state!,
            },
          },
        },
      });

      return NextResponse.json({ result: event, ok: true });
    }
  } catch (error) {
    console.error(error);

    return NextResponse.json(
      { message: "Something went worng", ok: false },
      { status: 500 }
    );
  }
}

Do you remember the success_url we configured in the stripeSession in the previous post? This should be the redirected URL after the user completes the payment.

const stripeSession = await stripe.checkout.sessions.create({
  success_url: `${process.env.NEXT_PUBLIC_SERVER_URL}/thank-you?orderId=${order.id}`, // will be redirected to this URL if payment succeed
  cancel_url: `${process.env.NEXT_PUBLIC_SERVER_URL}/configure/preview?${item.id}`,
  payment_method_types: ["card", "link"],
  mode: "payment",
  shipping_address_collection: {
    allowed_countries: ["US", "CA", "ID", "DE"],
  },
  metadata: {
    userId: user.id,
    orderId: order.id,
  },
  line_items: [{ price: product.default_price as string, quantity: 1 }],
});

Hope this helps. See you in the next post!