Summary
Stripe says the payment succeeded, but your site never hears about it. Orders stay unpaid, subscriptions do not activate, and the Stripe Dashboard shows webhook deliveries failing with a 400 and this message: "No signatures found matching the expected signature for payload". It also appears as "Webhook signature verification failed" or Stripe\Exception\SignatureVerificationException.
This guide gives the fast isolation steps first. If you would rather hand it over, SazM fixes a failing Stripe webhook as an affordable $99 fixed-price bug fix, delivered within 48 hours, entirely in writing: no calls, no project manager, one senior developer.
Where the Error Lives
Stripe signs every webhook body and sends the result in the Stripe-Signature header. Your code recomputes it from three inputs: the endpoint signing secret, the exact raw request body and the header timestamp. If any of the three is wrong, verification fails. Look for these lines:
Stripe\Exception\SignatureVerificationException: No signatures found matching the expected signature for payload
-> wrong signing secret, or the body was changed before verification
Stripe\Exception\SignatureVerificationException: Timestamp outside the tolerance zone
-> server clock drift, or a retried event verified much later
Webhook Error: No signatures found matching the expected signature for payload.
Are you passing the raw request body you received from Stripe? (Node.js)
-> a JSON body parser ran before your webhook route
storage/logs/laravel.log
/var/log/nginx/error.log
In the Stripe Dashboard, open Developers → Webhooks → your endpoint → Recent deliveries and read the response body Stripe received.
Quick Fix Breakdown (DIY)
- Use the right signing secret. Each endpoint has its own
whsec_...secret. The secret from a Dashboard endpoint is different from the one printed bystripe listen, and test mode differs from live mode. Re-copy it, then clear cached config on Laravel:
php artisan config:clear
php artisan config:cache
- Verify against the raw body. Do not decode and re-encode JSON before verifying. In plain PHP:
$payload = @file_get_contents('php://input');
$sig = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
$event = \Stripe\Webhook::constructEvent($payload, $sig, $endpointSecret);
In Laravel use $request->getContent(), not $request->all().
- Node.js / Next.js. Use the raw body on the webhook route only. In Express,
express.raw({ type: 'application/json' }); in a Next.js App Router route handler,await req.text()rather thanawait req.json(). - Exclude the route from CSRF. In Laravel, add the webhook path to the CSRF exception list so a 419 does not hide the real result. This is separate from signature checking.
- Check proxies and rewriting. A CDN, WAF or middleware that modifies the body (for example whitespace or compression changes) breaks the signature. Test by pointing a Stripe CLI listener straight at the app:
stripe listen --forward-to localhost:8000/stripe/webhook
stripe trigger payment_intent.succeeded
- Check the server clock. Stripe rejects timestamps outside a five-minute tolerance by default.
timedatectl status
- Confirm the header arrives. Log
Stripe-Signaturetemporarily; it should look liket=...,v1=.... A missing header means a proxy stripped it. - Return 2xx quickly and process idempotently. Stripe retries failed deliveries, so the handler must tolerate duplicates. See idempotent webhook processing.
If none of this isolates it within an hour, stop. Every hour of guessing is more unpaid orders.
Hand It Over: $99 Fixed, Within 48 Hours
Webhook signature bugs are usually small, but they hide between your framework, your proxy, your host and Stripe. One developer who reads the delivery log, reproduces the failure on staging and fixes the root cause ends the loop.
What you get for $99:
- A fixed price agreed before work starts. One webhook issue, one flat fee, no hourly meter.
- Delivery within 48 hours of access, verified on staging with Stripe test-mode events before anything touches live.
- Fully written, fully async. You send the site, the error and when it started. Questions, progress and proof arrive in writing, so your time zone does not matter.
- Written proof it works: the root cause in plain language, what changed, a log of a signed test event accepted, and a rollback note.
- Money-back guarantee if the agreed root cause cannot be resolved.
- Safe access only: a separate account, staging first, test-mode keys. Never send live secret keys or card data through the inquiry form.
Behind it: 20+ years in production (since Oct 2006) and 100+ projects in the portfolio, including Laravel and Next.js projects with Stripe payments.
Get your Stripe webhook fixed for $99 → · Queue an urgent fix · See past projects
Frequently Asked Questions
What does "No signatures found matching the expected signature for payload" mean?
Stripe's signature check failed. The signing secret is wrong, the raw body was altered before verification, or the timestamp is outside tolerance. The wrong endpoint secret is the most common cause.
Why does my Stripe webhook work with the CLI but fail in production?
The Stripe CLI prints its own signing secret for the CLI listener. The Dashboard endpoint has a different whsec_ secret, and live and test modes differ too. Use the secret that belongs to the endpoint actually sending the event.
How do I get the raw request body in Laravel, Express or Next.js?
Laravel: $request->getContent(). Express: express.raw({ type: 'application/json' }) on the webhook route. Next.js route handler: await req.text().
How much does it cost to fix a failing Stripe webhook?
SazM fixes one webhook issue for an affordable fixed price of $99, delivered within 48 hours, with a money-back guarantee if the agreed root cause cannot be resolved. Hourly work is open-ended, while a fixed price is agreed before work starts.
Do I need a call to explain the problem?
No. Send the site URL, the exact error and the Dashboard delivery log in writing. That written report becomes the test that proves the fix works.
Webhooks still failing after this? Get a Stripe webhook fix at a $99 fixed price, fully written and delivered within 48 hours.