Enviera

Stripe webhook signature verification failed in Next.js App Router

Stripe documents this error: “Webhook signature verification failed. Err: No signatures found matching the expected signature for payload.” Stripe signature troubleshooting, read October 31, 2026. In our synthetic Node SDK tests, signature rejection threw StripeSignatureVerificationError.

For a Next.js App Router webhook, pass the unchanged body from await request.text(), the stripe-signature header and the matching endpoint signing secret to Stripe's constructEvent(). Do not parse the body as JSON and then stringify it before verification. Stripe's linked App Router example uses the raw text directly. Stripe signature troubleshooting and App Router example, read October 31, 2026.

This guide covers snapshot-event signature verification with the Stripe Node SDK. Stripe documents a separate parser for thin event notifications. Follow the matching event format rather than exchanging parsers while diagnosing the signature. Stripe documentation, read October 31, 2026.

Start with the secret for the actual delivery path

Identify where the request came from before changing the handler:

Both start with whsec_, but the CLI secret and Dashboard endpoint secret are different. Check the value configured for the server process handling this request against the matching source. Make that comparison privately; do not paste the secret into a support message or the page. Stripe documentation, read October 31, 2026.

Expected observation: the configured signing secret matches the endpoint or forwarding session that delivered the request. A familiar prefix is not enough. In our local test, keeping the payload and header unchanged but using a different synthetic secret caused verification to fail.

Check the header before investigating JSON

Read request.headers.get('stripe-signature') in the route receiving the POST. If it is missing, stop and trace how this request reached the route. Do not replace a missing header with a made-up value or skip verification. Stripe's verifier needs the body, signature header and endpoint secret together. Stripe documentation and Next.js Route Handlers, read October 31, 2026.

Expected observation: the receiving handler has the signature header supplied with this request. Its presence alone does not establish that the signature is valid; constructEvent() performs that check. Stripe App Router example, read October 31, 2026.

Preserve the request body in the App Router handler

In app/api/webhooks/route.ts, read the body as text and give that exact string to the verifier. Parsing and reserializing JSON can change whitespace or key ordering while leaving the data looking equivalent. Stripe requires the original UTF-8 body. Stripe documentation, read October 31, 2026.

The following is a verification block for your existing POST handler. It assumes stripe is your existing server-side Stripe SDK instance. Keep your event-processing code after this block; this is not a complete payment handler.

// Inside export async function POST(request: Request)
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!endpointSecret) {
  return new Response('Webhook configuration missing', { status: 500 });
}

const signature = request.headers.get('stripe-signature');
if (!signature) {
  return new Response('Missing Stripe signature', { status: 400 });
}

const rawBody = await request.text();
let event;
try {
  event = stripe.webhooks.constructEvent(
    rawBody,
    signature,
    endpointSecret
  );
} catch {
  return new Response('Invalid Stripe signature', { status: 400 });
}

// Continue with your existing event-processing code using event.
// Return a successful response promptly; defer slow work (see [1] below).

This adapts Stripe's App Router example, which verifies the text body before processing the event, and Next.js Route Handlers, which exposes Web Request headers and request.text(). Both read October 31, 2026. The separate configuration/header responses are diagnostic choices in this example. For response timing, Stripe's timeout guidance says to defer complex logic and respond successfully promptly. Stripe delivery troubleshooting, read October 31, 2026.

Do not apply Pages Router bodyParser configuration to solve this App Router case. Next.js explicitly says Route Handlers do not need that additional configuration for webhooks. Next.js Route Handlers, read October 31, 2026.

Expected observation: the unchanged body, matching header and correct secret produce an event object. If the same header is checked against a reformatted body, verification rejects it. Our synthetic test below reproduced that difference.

Validate locally without a payment or external webhook

Use a disposable local directory with the Stripe Node package installed. Save the following as verify.cjs and run node verify.cjs. The strings are deliberately synthetic; do not replace them with real keys or signing secrets. Stripe documents generateTestHeaderString() for mocked signed webhook tests. Stripe Node README, read October 31, 2026.

const assert = require('node:assert/strict');
const Stripe = require('stripe');
const stripe = new Stripe('sk_test_synthetic_not_a_real_key');
const secret = 'whsec_synthetic_local_only';
const payload = JSON.stringify({id:'evt_synthetic',object:'event',type:'test.synthetic',data:{object:{}}},null,2);
const signature = stripe.webhooks.generateTestHeaderString({payload,secret});
async function verify(body,header,endpointSecret){
 const req=new Request('http://localhost/api/webhooks',{method:'POST',body,headers:{'stripe-signature':header}});
 return stripe.webhooks.constructEvent(await req.text(),req.headers.get('stripe-signature'),endpointSecret);
}
(async()=>{
 const event=await verify(payload,signature,secret);
 assert.equal(event.id,'evt_synthetic'); console.log('PASS untouched payload: evt_synthetic');
 for(const [name,body,s] of [['reserialized body',JSON.stringify(JSON.parse(payload)),secret],['wrong secret',payload,'whsec_wrong_synthetic']]){
  await assert.rejects(()=>verify(body,signature,s),e=>{assert.equal(e.type,'StripeSignatureVerificationError'); console.log('PASS '+name+': '+e.type);return true;});
 }
 console.log('PASS no external calls; SDK verification via in-memory Request.text() only');
})();

Our observed results with Node v22.16.0 and Stripe Node 23.1.0:

CheckObserved result
Untouched body, matching header and secretReturned event ID evt_synthetic
Same header and secret, JSON parsed and reserializedStripeSignatureVerificationError
Untouched body and header, different secretStripeSignatureVerificationError

The test signs the payload locally and feeds it through an in-memory Request.text() call. It makes no external webhook request or Stripe API call. It checks the SDK inputs, not your deployed route, request forwarding or payment-processing behavior. A passing result does not demonstrate that an order was fulfilled.

If these local checks pass but your endpoint still rejects a request, compare that endpoint's configured secret, received header and body-reading path against the working inputs above. Keep verification enabled while you investigate. This is our diagnosis order based on Stripe's documented inputs and the local test. Stripe documentation, read October 31, 2026.

If delivery fails before signature verification

A TLS delivery error is a separate branch of troubleshooting in Stripe's documentation. For that branch only, the custom-domain SSL checker checks DNS and certificates for your domain and its www twin. It does not check Stripe signatures. Use it only for a domain you own or are authorized to check. Stripe delivery troubleshooting and checker scope, read October 31, 2026.

Request a launch-safety check

For the broader launch review, request a launch-safety check.

Sources

Sources read on October 31, 2026. Publication/update date is unknown unless stated.