I am currently working on configuring a webhook utilizing the Stripe platform with NextJS version 13.2.3

Upon successfully checking out my cart using the built-in Stripe page, I am redirected to the successUrl route. During this process, my local test webhook is triggered as expected. However, I encountered some errors when attempting to verify that the request originated from Stripe.

Below is my webhook setup in NextJS

import { stripe } from "@/lib/stripe"

export async function POST(req: Request){

    const payload = await req.json()
    
    const sig = req.headers.get('stripe-signature') as string
    console.log("sig: ", sig)

    let event;

    const endpointSecret = process.env.WEBHOOK_SECRET_LOCAL as string

    try {
        console.log("constructing event...")
        event = stripe.webhooks.constructEvent(payload, sig, endpointSecret)
    } catch (error) {
        console.error(error)
        return new Response(`Webhook error: ${error}`, {
            status: 400,
        })
    }
    
    return new Response("payment confirmation route received", {
        status: 200,
    })
}

I've utilized the Stripe CLI to listen for the success event

stripe listen --forward-to localhost:3000/api/checkout/payment-confirmation

After completing my cart checkout, the following events are triggered...

2023-03-23 15:51:28   --> checkout.session.completed [evt_1MowyjE7TMviQOgJNbXch10w] 2023-03-23 15:51:28   --> charge.succeeded [evt_3MowyhE7TMviQOgJ09e3Q7Hu] 2023-03-23 15:51:29   --> payment_intent.succeeded [evt_3MowyhE7TMviQOgJ0OIjeNoH] 2023-03-23 15:51:29   --> payment_intent.created [evt_3MowyhE7TMviQOgJ0ovhouJM] 2023-03-23 15:51:32  <--  [400] POST http://localhost:3000/api/checkout/payment-confirmation [evt_3MowyhE7TMviQOgJ09e3Q7Hu] 2023-03-23 15:51:32  <--  [400] POST http://localhost:3000/api/checkout/payment-confirmation [evt_3MowyhE7TMviQOgJ0OIjeNoH] 2023-03-23 15:51:32  <--  [400] POST http://localhost:3000/api/checkout/payment-confirmation [evt_1MowyjE7TMviQOgJNbXch10w] 2023-03-23 15:51:32  <--  [400] POST http://localhost:3000/api/checkout/payment-confirmation [evt_3MowyhE7TMviQOgJ0ovhouJM]

Upon inspecting the NextJS logs, I noticed...

StripeSignatureVerificationError: Webhook payload must be provided as a string or a Buffer (https://nodejs.org/api/buffer.html) instance representing the _raw_ request body.Payload was provided as a parsed JavaScript object instead. Signature verification is impossible without access to the original signed material. Learn more about webhook signing and explore webhook integration examples for various frameworks at https://github.com/stripe/stripe-node#webhook-signing

My interpretation of this error led me to believe that I needed to manipulate the payload argument. I attempted payload.toString(), JSON.stringify(payload), and using const buf = Buffer.from(payload), among other methods, in an effort to convert the object into a string or buffer. However, I am still struggling to grasp the issue. Any assistance would be greatly appreciated!

--Update--

I have made adjustments to the code, but I am still encountering the generic error...

No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe?

My response to Stripe: "I believe so?!?"

I also experimented with importing 'micro' or 'raw-body' as observed in other implementations, but encountered various errors. This prompted me to revert to the node approach. Interestingly, when passing the req.body as the first argument in constructEvent, I encountered the error message:

Argument of type 'ReadableStream | null' is not assignable to parameter of type 'string | Buffer'

👆

import { stripe } from "@/lib/stripe"
import { NextRequest } from "next/server";
import { Buffer } from "node:buffer";

export async function POST(req: NextRequest){
    
    const buf = Buffer.from(req.toString())
// <Buffer 5b 6f 62 6a 65 63 74 20 52 65 71 75 65 73 74 5d>

    const headers = req.headers
    
    const sig = req.headers.get("stripe-signature") as string | string[]
// t=1679...,v1=888050e17...,v0=cc9b94a...
    let event;
    const endpointSecret = process.env.WEBHOOK_SECRET_LOCAL as string
// whsec_...
    

    try {
        console.log("constructing event...")
        event = stripe.webhooks.constructEvent(buf, sig, endpointSecret)
    } catch (error) {
        console.error(error)
        return new Response(`Webhook error: ${error}`, {
            status: 400,
        })
    }
    
    return new Response("payment confirmation route received", {
        status: 200,
    })
}

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

Answer №1

For optimal performance, refrain from utilizing micro for buffering the body. Instead, opt for await req.text() to transform the raw body into a string, after which you can pass this string to stripe.webhooks.constructEvent.

Answer №2

Have you been experiencing issues with webhooks functioning on localhost but not after deployment? If you have exhausted all other troubleshooting steps, the problem may lie in using the incorrect webhook secret. It is advised to avoid using the secret provided on the latest Stripe tutorial page, even during test mode, as it is only reliable for localhost testing.

https://i.sstatic.net/L12Pn.png

Instead, ensure to obtain the webhook secret from this source (by clicking "Reveal")

https://i.sstatic.net/1f20N.png

Answer №3

I've successfully implemented this code snippet. Feel free to reach out if you have any inquiries.

import Stripe from 'stripe';
import { headers } from 'next/headers';

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

export async function POST(req: Request) {
  const body = await req.text();
  const sig = headers().get('Stripe-Signature') as string;
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
  let event: Stripe.Event;

  try {
    if (!sig || !webhookSecret) return;
    event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
  } catch (err: any) {
    console.log(`❌ Error message: ${err.message}`);
    return new Response(`Webhook Error: ${err.message}`, { status: 400 });
  }

  if (relevantEvents.has(event.type)) {
    try {
      switch (event.type) {
        case 'product.created':
          break;
        case 'product.updated':
          break;
        default:
          throw new Error('Unhandled relevant event!');
      }
    } catch (error) {
      console.log(error);
      return new Response('Webhook handler failed. View logs.', {
        status: 400
      });
    }
  }
  return new Response(JSON.stringify({ received: true }));
}

Answer №4

When Stripe sends an Event to your webhook endpoint, they initially calculate a signature based on the raw body of the Event. It is crucial that when processing the data, you ensure that you are examining the exact raw body that Stripe utilized. Any alterations, even as minor as adding a single space or changing the order of a property, will result in the signature not matching.

Many developers using stripe-node often encounter difficulties with signature verification. This is because various frameworks automatically parse JSON data they receive for easy consumption. As a result, when attempting to access the data, a different version of the payload may be retrieved even if the information within is similar.

If your code is utilizing req.json(), it is essential to access the precise raw body of the POST request, typically done through request.body. Consult Stripe's documentation here for guidance. Additionally, reviewing this popular issue on the Github repository for stripe-node can provide various alternative solutions tailored to your specific environment.

Answer №5

One challenge arises from Stripe's requirement for the raw body to compute a signature, while Next.js automatically parses the body. To address this, you can disable the body parser and process the data as a buffer. Here's a solution:

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

const buffer = (req) => {
  return new Promise((resolve, reject) => {
    const chunks = [];

    req.on('data', (chunk) => {
      chunks.push(chunk);
    });

    req.on('end', () => {
      resolve(Buffer.concat(chunks));
    });

    req.on('error', reject);
  });
};

const handler = async (req, res) => {
  if (!req.method === 'POST') {
    res.setHeader('Allow', 'POST');
    res.status(405).end('Method Not Allowed');
    return;
  }

  const sig = req.headers['stripe-signature'];

  let event;

  try {
    const body = await buffer(req);

    event = stripe.webhooks.constructEvent(
      body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (error) {
    console.log('Webhook invalid!', error);
    return res.status(400).send(`Webhook Error: ${error.message}`);
  }

  // Handle the checkout.session.completed event
  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;

    // Fulfill the purchase...
    console.log('SESSION: ', session);
  }

  // Return a response to acknowledge receipt of the event
  res.json({ received: true });
};

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

export default handler;

This approach is inspired by:

https://github.com/stripe/stripe-node/blob/master/examples/webhook-signing/nextjs/pages/api/webhooks.ts

Referenced from: https://github.com/stripe/stripe-node#webhook-signing

Trust this explanation proves helpful!

Answer №6

Recently, I completed a project with Next.js that involved integrating Stripe and a webhooks API. The project utilized the new /app router. If you're interested, you can check out the README.md by visiting the repository link: https://github.com/BastidaNicolas/nextauth-prisma-stripe

Answer №7

To set up a Stripe webhook, you will require the raw body content.

In Next.js version 13, you can access the raw body data with the following code:

const rawBody = await req.text()

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is there a way to assign a value to an Angular-specific variable using PHP?

In the development of my Angular 4 application, I encountered an issue while receiving JSON data based on an id value through a PHP script. Upon examining the code, it seems that there should be a value passed into this.PropertiesList. examineProperties(i ...

The error message "@graphql-eslint/eslint-plugin: problem with the "parserOptions.schema" configuration"

Our team is currently working on developing micro-services using NestJS with Typescript. Each of these services exposes a GraphQL schema, and to combine them into a single graph, we are utilizing a federation service built with NestJS as well. I recently ...

The action dispatched by "AuthEffects.register$" is not valid and will have no effect

After implementing a new effect for the register action, I encountered the following error message: Effect "AuthEffects.register$" dispatched an invalid action Below is the code for my effect: @Effect() register$ = this.actions$.pipe( ofType<Regis ...

Using the React Icon component as a prop in the Client Component within a NextJS application

Creating a dynamic button: 'use client'; import type { IconType } from 'react-icons'; interface ButtonProps { children: React.ReactNode; Icon: IconType; } export default function Button(props: ButtonProps) { const { children, ...

Enhancing Security and Privacy of User Information with JWT Tokens and NgRx Integration in Angular Application

I'm facing a security concern with my Angular application. Currently, I store user details like isAdmin, isLoggedIn, email, and more in local storage. However, I'm worried about the risks of unauthorized updates to this data, especially since my ...

Zod Entry using standard encryption key

I'm attempting to define an object type in zod that looks like this: { default: string, [string]: string, } I've experimented with combining z.object and z.record using z.union, but the validation results are not as expected. const Local ...

How can I change an icon and switch themes using onClick in react js?

I have successfully implemented an icon click feature to change the colorscheme of my website (in line 21 and changeTheme). However, I also want the icon to toggle between FaRegMoon and FaRegSun when clicked (switching from FaRegMoon to FaRegSun and vice v ...

Retrieve the property of a Typescript object using a template argument

I am looking to develop a Typescript Collection class that can locate items by field. Here is an example of what I have in mind: class Collection<T, K keyof T> { private _items: T[]; public isItemInCollection(item: T) { return _item ...

A guide to mocking Prisma using Jest mock functionality

Utilizing prisma for database interactions and eager to implement jest-mock to simulate the findMany call. https://jestjs.io/docs/jest-object#jestmockedtitem-t-deep--false brands.test.ts import { PrismaService } from "@services/mysql.service"; i ...

"Error message: Antd datepicker is throwing an error stating that date.clone/date.load is not a

I am working on a React app that has a checkbox to disable a datepicker. However, when I use the checkbox to disable it, I am unable to select any date. If I remove the checkbox and its function, there is no error. Currently, the error message I am getting ...

Maximum Age Setting for Iron Session Cookie

Currently, I am attempting to configure a Next JS application with iron-session and a 'remember me' feature. The objective is for the maxAge of the iron-session cookie to be extended to a week if the user selects the remember me option on the log ...

Is it possible to modify the parameters of a function by utilizing a MethodDecorator without affecting the "this" value?

Consider a scenario where you need to dynamically modify method arguments using a decorator at runtime. To illustrate this concept, let's simplify it with an example: setting all arguments to "Hello World": export const SillyArguments = (): MethodDec ...

Sharing parameters between pages in Angular IonicPassing parameters between pages within an Angular Ionic application

Is there a way to pass parameters from the signup page to the signupotp page successfully? I am facing an issue where the OTP on the signupotp page is not being recognized because the parameters (email and mobile) are not getting passed properly. In my bac ...

Having trouble with obtaining real-time text translation using ngx translate/core in Angular 2 with Typescript

Issue : I am facing a challenge with fetching dynamic text from a JSON file and translating it using the translate.get() method in Angular2. this.translate.get('keyInJson').subscribe(res => { this.valueFromJson = res; /* cre ...

Is there a different option similar to forkJoin for handling incomplete observables?

constructor( private route: ActivatedRoute, private http: Http ){ // Retrieve parameter changes observable let paramObs = route.paramMap; // Fetch data once only let dataObs = http.get('...'); // Subscribe to both ob ...

Eliminate the eslint@typescript-eslint/no-unused-vars error in TypeScript when the index parameter is not utilized within a map function

Here's the code snippet in question: const reducer = (element:number, index: number) => [element]; //eslint-message. const positionsArray = $.map(this.positions, reducer); I am converting a Float32Array (this.positions) to a JavaScript array. The ...

Issue with Nextjs: Page style fails to load initially leading to oversized icons and broken CSS

I'm experiencing an issue with my page where the icons appear oversized and the CSS seems to be broken upon loading. I followed the official Next.js MUI5 example. "next": "^12.3.1", "react": "^18.2.0", &quo ...

Discovering the proper method for indicating the type of a variable in the middle of a statement

In my code, there was a line that looked like this: let initialPayload = this.db.list("/members").snapshotChanges() as Observable<any[]> But then I changed it to this: let initialPayload = this.db.list("/members").snapshotChanges ...

Guidelines for forming a composite type with elements?

Imagine having a convenient function that wraps a generic component with a specified constant. function wrapComponent(ComponentVariant: ComponentVariantType) { return ( <Wrapper> <ComponentVariant> <InnerComponent /> ...

Checkbox offering a tri-state option

Seeking help on creating a checkbox with three states. I have implemented a method to toggle between these states upon clicking. However, the issue is that this method is only triggered after the HTML changes. As a result, the checkbox's navigation be ...