TutorialsCourses

Stripe Webhook Verification with NextJS

NextJS API Route

NextJS provides file based routing, even for API routes. API routes are files that are created inside of the pages/api folder. By default the API route tries to be as helpful as possible, meaning it will parse the request body based upon the incoming Content-Type in the headers.

So when a POST comes in with Content-Type application/json you can expect that req.body will be the parsed payload.

One issue is that with Stripe it needs to use the raw payload to validate the request actually originated from Stripe. You should always validate your Stripe webhook requests, if someone found out your webhook URL you could have people sending fake requests.

To disable this default parsing behavior API routes from Next.js have a config export option. If you export config you can set false on api.bodyParser. This will disable the above behavior and allow us to verify the raw request.

// pages/api/stripe_hook.js

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

const handler = async (req, res) => {
  if (req.method === "POST") {
    // Code here
  } else {
    res.setHeader("Allow", "POST");
    res.status(405).end("Method Not Allowed");
  }
};

export default handler;

Setup Stripe

Now to get stripe setup and verifying install the stripe package, and import it. We need 2 pieces of information that should come from environment variables. The Stripe secret, as well as the signing key for this specific webhook.

These can both be found in the stripe dashboard.

import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: "2020-08-27",
});
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;

Verify the Request

Now that we Stripe installed another lets install micro, a library built by the team from Vercel. It provides small functions that help dealing with requests. In this case we want to convert our req to a buffer.

Under the hood it does a few which you can check out here https://github.com/vercel/micro/blob/master/packages/micro/lib/index.js#L136

The stripe.webhooks.constructEvent function takes 3 arguments. The first being a buffer or string. In this case we pass in a buffer. The second is the stripe-signature that is on the request headers. The third is our webhook secret.

If all goes well the returned value will be the event, and it handles all the body parsing correctly for you. Otherwise if it failed it will throw an error which is why we wrap it in a try/catch and return an error for the webhook.

import { buffer } from "micro";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: "2020-08-27",
});
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;

const handler = async (req, res) => {
  if (req.method === "POST") {
    const buf = await buffer(req);
    const sig = req.headers["stripe-signature"];

    let event;

    try {
      event = stripe.webhooks.constructEvent(buf, sig, webhookSecret);
    } catch (err) {
      res.status(400).send(`Webhook Error: ${err.message}`);
      return;
    }

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

export default handler;

The Events

Afterwards you can use the event object to deal with all the stuff you need to do. Check if a charge succeeded, or if you received an event you weren't expecting log it out. By this part in the code you know you have received an event that actually originated from Stripe.

if (event.type === "charge.succeeded") {
  const charge = event.data.object;
  // Handle successful charge
} else {
  console.warn(`Unhandled event type: ${event.type}`);
}