Stripe Webhook DoctorTools for workARLing s. r. o., Bratislava

See why Stripe says your webhook signature is invalid.

Paste the exact error text, how your handler reads the request body and the Stripe-Signature header, and where your whsec_ secret comes from. Get the one cause that's actually wrong and a copy-paste fix for Next.js, Express, Fastify, NestJS, Django, Flask, FastAPI, Rails, Laravel, Go, .NET, or serverless.

Diagnose yours Source code on GitHub

Free
Every check and every finding, with no account and no limit.
Paid
Nothing. This tool has no paid version and no sign-up.
Limits
16 frameworks and runtimes covered, behind 125 automated tests.
Your data
Nothing leaves your browser. There is no backend of ours behind this page.
The tool after a diagnosis: a verdict and the rules that fired, each with the header or body part it points at.
A real diagnosis of the request the tool opens with, run on this page. Nothing was sent to a server of ours; you can repeat it below.
fn StripeWebhookDoctor.diagnose(config)
client-side, no network
Your error
From your server logs, or Stripe Dashboard → Webhooks → your endpoint → Event deliveries.
How your handler gets the body
Your secret
Timing
Difference between your server's clock and real time, if you've checked.
Runtime & proxy
Only relevant behind AWS API Gateway.
Your handler code (optional)
Checked with a few plain-text patterns in your browser only: never executed, never sent anywhere.
Result · idle
Fill in the form and press Diagnose or ⌘↵ to see the result.
status
idle
not run yet

Three reasons a webhook that looks configured still fails signature verification.

Stripe's own troubleshooting docs say a "No signatures found" error means exactly one of three parameters is wrong. These three causes account for almost every case.

  1. The body isn't raw anymore.

    constructEvent needs the exact bytes Stripe sent. A global JSON body-parser, a JSON.parse, or req.json() instead of req.text() all change those bytes before verification runs.

  2. The wrong secret.

    A Dashboard endpoint and a running stripe listen each generate their own whsec_ secret. Test mode and live mode each have a separate one too, even on the same endpoint URL.

  3. Timing or the header.

    A server clock more than 5 minutes off real time trips the tolerance check. A header read under the wrong name, or an edge runtime calling the synchronous constructEvent instead of the async version, fails before the HMAC is even compared.

One function. Config in, diagnosis out.

No server, no API key, no signup. doctor-stripe.js is plain JavaScript: read it, fork it, or run it in your own scripts or CI.

doctor-stripe.js 0 dependencies
import { diagnose } from './doctor-stripe.js';
// or, loaded globally: const { diagnose } = window.StripeWebhookDoctor;

const result = diagnose({
  error: { message },
  app:   { framework, bodyHandling, usesBodyParserGlobally, readsHeader,
           secretSource, secretPrefix, mode, endpointsCount, tolerance,
           runtime, proxy, bodyBase64, clockSkewSeconds },
  code:  { snippet },
});

// result
{
  "status":     "fail" | "warn" | "pass",
  "summary":    "…",
  "expected":   { framework, secretSource, bodyAccess: { code, note } },
  "problems":   [ { severity, code, message, path, value, fix } ],
  "fixes":      [ { title, value, where } ],
  "checklist":  [ "…" ],
  "disclaimer": "…"
}

Everything runs client-side. The form above calls this exact function in your browser. There is no backend, no API key, and no request that carries your config anywhere.

It classifies the error text you paste against five known constructEvent/signature error strings, checks raw-body handling, the secret's source and prefix, the Stripe-Signature header, tolerance and clock skew, and edge/proxy quirks like API Gateway's base64 body or a Cloudflare Worker's missing constructEventAsync, and reports which one actually applies, with a framework-specific fix.

Free and open source. Found a case it gets wrong? Open a GitHub issue on the repo. No ads, and no tracking beyond anonymous usage counts.

Questions developers actually search for.

Straight answers to the same webhook signature questions this tool diagnoses, for when you just need the answer, not the checker.

Why does Stripe say no signatures found matching the expected signature?

Stripe's own troubleshooting docs say this comes from exactly one of three parameters being wrong: the request body, the Stripe-Signature header, or the endpoint secret. In practice the most common cause is the wrong secret (a Dashboard endpoint's whsec_ used with a stripe listen event, or the other way round, or an API key pasted in by mistake). The second most common cause is the raw request body being changed before it reaches constructEvent: parsed to JSON, re-serialized, or consumed by a global body-parser middleware first.

How do I get the raw body in Next.js App Router?

Read it with await req.text(), never await req.json(). stripe-node's own App Router example does exactly this: const body = await req.text(); then stripe.webhooks.constructEvent(body, signature, secret). req.json() parses the body into an object first, which changes the bytes constructEvent needs to verify against the signature.

Which webhook secret do I use: Dashboard or stripe listen?

Whichever one actually sent you the request. A webhook endpoint you create in the Dashboard has its own whsec_ secret, shown once you click Reveal secret. Running stripe listen locally prints a separate secret in your terminal, generated just for that forwarding session. Stripe's own docs warn: don't verify signatures on events forwarded by the CLI using the secret from a Dashboard-managed endpoint, or the other way around.

What does timestamp outside the tolerance zone mean?

The timestamp Stripe signed into the Stripe-Signature header is further from your server's current clock time than your tolerance window allows. Stripe's libraries default this tolerance to 300 seconds (5 minutes). The usual cause is a server clock that has drifted out of sync; Stripe recommends using NTP to keep it accurate. Setting the tolerance to 0 does not fix this: Stripe's docs warn that a tolerance of 0 disables the recency check entirely, rather than making it stricter.

Do I need a different secret for test and live mode?

Yes. Stripe's own docs state it directly: if you use the same endpoint for both test and live API keys, the secret is different for each one. If your endpoint works in test mode but fails in live mode (or the reverse), check that you copied the whsec_ value for the mode that is actually sending you events, not the other one.

Does this tool send my secret anywhere?

No. Everything runs in your browser. You are only asked to describe your configuration in general terms (framework, how the body is read, which secret prefix you're using, whether the source is a Dashboard endpoint or stripe listen); the tool never asks for the secret value itself, and nothing you type is sent to a server.

Free.

No account, no payment, no usage limit: it runs as a static page in your browser, so there is no server to bill for.

Who publishes this and how it works.

Publisher
ARLing s. r. o., Ivanská cesta 32E, 821 04 Bratislava, Slovakia. Company number 56583486, VAT ID SK2122352100.
Who makes it
Written and maintained by one person in Bratislava. The source is public on GitHub.
Payment and delivery
Nothing is paid for and nothing is unlocked. The answer appears in the page as soon as the check runs.
Sample and terms
The tool opens with a worked example, so you can see a real result before you paste your own.
Reply
We answer andrej@arling.sk within 24 hours.
What it is not
We are not the vendor whose error you are chasing. The tool reads what you paste and names the rule; it changes nothing on your side.

Terms Privacy