← All errors · StripeStripe payments & webhook errors

Webhook payload must be provided as a string or a Buffer instance representing the _raw_ request body

Exact error

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.

What it means

stripe.webhooks.constructEvent() needs the exact bytes Stripe sent so it can check the Stripe-Signature header. Something in your app already turned the body into a JavaScript object (JSON parsing), so the original text is gone and the signature cannot be verified. A close cousin, "No webhook payload was provided.", means the body reached Stripe's library empty.

Likely causes

How to fix it

  1. Next.js App Router (app/api/webhooks/stripe/route.js): read the body with req.text() and pass that string straight to constructEvent:

    import Stripe from 'stripe';
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
    
    export async function POST(req) {
      const body = await req.text(); // raw text, not req.json()
      const sig = req.headers.get('stripe-signature');
      let event;
      try {
        event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET);
      } catch (err) {
        return new Response(`Webhook Error: ${err.message}`, { status: 400 });
      }
      // handle event.type here
      return new Response('ok');
    }
  2. Express: give the webhook route express.raw() and register it before app.use(express.json()), so JSON parsing never touches it:

    app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
      const sig = req.headers['stripe-signature'];
      let event;
      try {
        event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
      } catch (err) {
        return res.status(400).send(`Webhook Error: ${err.message}`);
      }
      res.json({ received: true });
    });
    
    app.use(express.json()); // other routes, after the webhook
  3. Next.js Pages Router (pages/api/webhook.js): turn off the built-in body parser and read the raw stream yourself:

    export const config = { api: { bodyParser: false } };
    
    async function rawBody(req) {
      const chunks = [];
      for await (const chunk of req) chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
      return Buffer.concat(chunks);
    }
    
    export default async function handler(req, res) {
      const buf = await rawBody(req);
      const event = stripe.webhooks.constructEvent(buf, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
      res.json({ received: true });
    }
  4. Supabase Edge Function (common in Lovable and Bolt apps): use req.text() and the async version of constructEvent, because Deno uses the Web Crypto API:

    const body = await req.text();
    const sig = req.headers.get('stripe-signature');
    const event = await stripe.webhooks.constructEventAsync(
      body, sig, Deno.env.get('STRIPE_WEBHOOK_SECRET'),
      undefined, Stripe.createSubtleCryptoProvider()
    );
  5. Test it locally with the Stripe CLI and check the webhook's delivery attempts in the Stripe Dashboard. If the error changes to "No signatures found matching the expected signature for payload", the body is now raw but the signing secret is wrong for that endpoint (see the related fix).

    stripe listen --forward-to localhost:3000/api/webhooks/stripe
    stripe trigger checkout.session.completed

Prevent this next time

FAQ

What does "Webhook payload must be provided as a string or a Buffer instance representing the _raw_ request body" mean?

stripe.webhooks.constructEvent() needs the exact bytes Stripe sent so it can check the Stripe-Signature header. Something in your app already turned the body into a JavaScript object (JSON parsing), so the original text is gone and the signature cannot be verified. A close cousin, "No webhook payload was provided.", means the body reached Stripe's library empty.

What usually causes it?

Express has app.use(express.json()) registered before the webhook route, so the body is parsed before your handler sees it. A Next.js App Router route handler calls await req.json() and passes the result to constructEvent. A Next.js Pages Router API route (pages/api/...) still has the default body parser on. The handler passes req.body after a framework or middleware already parsed it, or calls JSON.stringify(req.body) to "fix" it. Re-stringified JSON is not byte-for-byte identical, so that fails with a signature mismatch instead. In a Supabase Edge Function, the code uses await req.json() instead of await req.text().

How do I fix it?

1. Next.js App Router (app/api/webhooks/stripe/route.js): read the body with req.text() and pass that string straight to constructEvent: 2. Express: give the webhook route express.raw() and register it before app.use(express.json()), so JSON parsing never touches it: 3. Next.js Pages Router (pages/api/webhook.js): turn off the built-in body parser and read the raw stream yourself: 4. Supabase Edge Function (common in Lovable and Bolt apps): use req.text() and the async version of constructEvent, because Deno uses the Web Crypto API: 5. Test it locally with the Stripe CLI and check the webhook's delivery attempts in the Stripe Dashboard. If the error changes to "No signatures found matching the expected signature for payload", the body is now raw but the signing secret is wrong for that endpoint (see the related fix).

Last checked against official docs on . Sources: Stripe docs: resolve webhook signature verification errors, Stripe docs: webhook quickstart.

Related errors

More Stripe errors and fixes →

Get new fixes weekly

No spam. Unsubscribe anytime. Privacy