Building a Payment Flow in Next.js 13 Using Stripe, Mailsender, and Webhooks
Tags: stripe, nextjs, webhooks, webdev
Published: 2023-08-02 10:33:53

**Introduction**:
In my latest project, [WanderKit.net](wanderkit.net), users can effortlessly generate **personalised itineraries** for their upcoming trips using **AI**. To enhance the user experience and provide a seamless journey, I decided to implement a payment flow feature. This would allow users to purchase the generated itineraries and have them securely stored in our database, enabling easy access and integration with their calendars. In this blog, I'll walk you through the process of setting up the payment flow using **Stripe**, integrating with **Mailsender** for confirmation emails, and handling **webhooks** for successful payments.

**Requirements**:
Before starting, ensure you have the following API keys from Stripe, obtained after signing up for their services. Use the test keys during development:

```env
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=""
STRIPE_SECRET_KEY=""
STRIPE_WEBHOOK_SECRET=""
```

**Creating the Checkout Endpoint:**
The first step was to create a checkout endpoint on the server-side. This endpoint creates a checkout session and returns it to the client.


```ts
// src/app/api/checkout/route.ts

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

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
  apiVersion: "2022-11-15",
});

export async function POST(req: Request) {
  try {
    const body = await req.json();

    if (!body.itineraryId) {
      throw new Error("Missing itinerary_id");
    }

    const params: Stripe.Checkout.SessionCreateParams = {
      submit_type: "pay",
      payment_method_types: ["card"],
      mode: "payment",
      line_items: [
        {
          price_data: {
            currency: 'usd',
            product_data: {
              name: "Itinerary",
            },
            unit_amount: 100,
          },
          quantity: 1,
        },
      ],
      metadata: {
        itinerary_id: body.itineraryId,
      },
      success_url: `${process.env.URL}/success?itineraryId=${body.itineraryId}`,
      cancel_url: `${process.env.URL}/`,
    };

    const checkoutSession: Stripe.Checkout.Session =
      await stripe.checkout.sessions.create(params);

    return NextResponse.json({ result: checkoutSession, ok: true });
  } catch (error) {
    console.error(error);
    return NextResponse.json(
      { message: "something went wrong", ok: false },
      { status: 500 }
    );
  }
}

```

**Client-Side Payment Handling**:
On the client-side, I implemented a handle payment function connected to a button. This function calls the previously created endpoint with the necessary data (in this case, the itinerary). Upon response, it redirects the user to the checkout page, handling any potential errors.

```ts
export default function Home() {
  // ...

  const handlePayment = async () => {
    try {
      if (!itinerary) {
        throw new Error("Something went wrong");
      }

      const response = await fetch("api/checkout/", {
        method: "post",
        body: JSON.stringify({ itineraryId: itinerary.id }),
      });

      const json = await response.json();

      if (!json.ok) {
        throw new Error("Something went wrong");
      }

      const stripe = await getStripe();

      if (!stripe) {
        throw new Error("Something went wrong");
      }

      await stripe.redirectToCheckout({ sessionId: json.result.id });
    } catch (error) {
      toast("Failed to start transaction", "Please try again.");
    }
  };

  return (
    // ...
    <Button onClick={handlePayment}>Purchase</Button>  
    // ...
  )

}
```

**Setting Up Webhooks**:
**Webhooks** are essential for receiving real-time updates from Stripe once the payment is successful. It ensures we process the payment confirmation securely. Stripe verifies its signature by parsing the raw string rather than the request body. In the webhook, I checked if the event type was checkout.session.completed, and if so, I updated the database accordingly and sent the user a verification email using **Mailsender**.

```ts
// src/app/api/webhooks/checkout/route.ts

import Cors from "micro-cors";
import { headers } from "next/headers";
import { NextResponse } from "next/server";

const stripe = require("stripe")(process.env.STRIPE_PRIVATE);

const cors = Cors({
  allowMethods: ["POST", "HEAD"],
});

const secret = process.env.STRIPE_WEBHOOK_SECRET || "";

export async function POST(req: Request) {
  try {
    const body = await req.text();

    const signature = headers().get("stripe-signature");

    const event = stripe.webhooks.constructEvent(body, signature, secret);

    if (event.type === "checkout.session.completed") {
      if (!event.data.object.customer_details.email) {
        throw new Error(`missing user email, ${event.id}`);
      }

      if (!event.data.object.metadata.itinerary_id) {
        throw new Error(`missing itinerary_id on metadata, ${event.id}`);
      }

      updateDatabase(event.data.object.metadata.itinerary_id);
      sendEmail(event.data.object.customer_details.email);

    }

    return NextResponse.json({ result: event, ok: true });
  } catch (error) {
    console.error(error);
    return NextResponse.json(
      {
        message: "something went wrong",
        ok: false,
      },
      { status: 500 }
    );
  }
}

```


**Testing Locally**:
To test the payment flow locally, I installed the Stripe CLI [https://stripe.com/docs/stripe-cli](https://stripe.com/docs/stripe-cli) and added a script to the package.json file. Running "stripe:listen" ensures that webhooks are directed to our local project while developing. I could then click through the app, proceed with a test payment (in test mode using a provided credit card number), and verify that the webhook was triggered.

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

Manually trigger a webhook with the following command, adding whatever parameters you need for testing.

```bash
stripe trigger checkout.session.completed --add checkout_session:metadata.itinerary_id=123
```

**Setting Up Webhooks for Production**:
After successful testing, I configured a new webhook in the Stripe developer dashboard for the production environment, ensuring the smooth operation of the payment flow in the live system. [https://dashboard.stripe.com/webhooks](https://dashboard.stripe.com/webhooks)

Conclusion:
By integrating Stripe, Mailsender, and webhooks into my Next.js 13 app, [WanderKit.net](wanderkit.net) now offers users a convenient payment flow to purchase their AI-generated itineraries. The entire process is secure and efficient, providing travelers with an unforgettable journey planning experience!

Checkout the live product at [wanderkit.net](wanderkit.net), read my other blogs on [levischouten.net](levischouten.net)