Knowledge Base

Webhook debugging guides

Honest command-line answers to the errors Stripe, GitHub, Shopify, Slack, Polar, Svix, Paddle, Lemon Squeezy and Twilio actually show you, and where WebhookMon makes it easier.

Stripe webhook signature verification failed

"No signatures found matching the expected signature for payload" usually means the wrong secret, a body a framework re-parsed before verifying, or a replay older than Stripe's 5-minute tolerance. Here's how to tell which.

Test Stripe webhooks locally without ngrok

stripe listen --forward-to forwards Stripe's test events to your machine over an outbound connection, no tunnel needed. It's Stripe-only, works only while the CLI runs on that machine, and signs with its own whsec_ secret.

GitHub: "We couldn't deliver this payload"

GitHub gives your server a few seconds to answer. Service Unavailable or a timeout means it didn't. Redeliver from the repo's webhook settings, or forward live events locally with gh webhook forward.

Validating Shopify webhook HMAC signatures

X-Shopify-Hmac-Sha256 is a base64 HMAC-SHA256 over the raw request body, keyed with your app's client secret, not its API key. Node and Python code that gets the raw-body part right.

Slack: "Your URL didn't respond with the value of the challenge parameter"

Slack's first request is a url_verification event; echo its challenge field back within 3 seconds, before you do anything else with the request.

Verifying Polar webhooks (Standard Webhooks)

Polar signs webhooks using the Standard Webhooks spec: webhook-id, webhook-timestamp and webhook-signature headers. Newer whsec_ secrets are used as-is; older ones need base64-encoding first.

Stripe webhook signature fails in Next.js, NestJS or Fastify: getting the raw body

constructEvent needs the exact bytes Stripe sent. How to get the raw body in the App Router, the Pages Router, NestJS and Fastify, and a one-line length check that proves it's the body, not the secret.

Verifying Slack request signatures (X-Slack-Signature, v0=)

HMAC-SHA256 of v0:timestamp:rawBody with your Signing Secret, not the Verification Token. Node and Python code, the seconds-vs-milliseconds trap, and six reasons it fails with the right secret.

Verifying GitHub webhook signatures (X-Hub-Signature-256)

sha256= plus a hex HMAC-SHA256 of the raw body, keyed with the webhook's secret. Node and Python code checked against GitHub's own test values, and why it fails: a parsed body, the wrong secret, or the legacy SHA-1 header.

Verifying Svix webhooks (svix-id, svix-timestamp, svix-signature)

How Clerk, Resend and other Svix senders sign webhooks, the svix library in Node and Python, a manual check that handles whsec_ keys and multiple signatures, and what "No matching signature found" means.

Webhook timestamp expired when replaying a captured event

Stripe, Slack and Polar sign a timestamp along with the body and reject one more than 5 minutes old, even with the right secret. Replaying a saved event means re-signing it with a current timestamp.

Paddle webhook signature verification failed (Paddle-Signature)

h1 is a hex HMAC-SHA256 of ts:rawBody with the destination's pdl_ntfset_ secret key. Paddle's SDKs also reject events more than 5 seconds old, so replays fail with the right secret. Node and Python code and a curl test.

Stripe: "Timestamp outside the tolerance zone"

The signature matched but its t= is more than 300 seconds before your server's clock: a local replay, a delayed queue, or clock skew. How to pass tolerance in every Stripe SDK, verify at receipt instead of in the worker, and why a Dashboard resend never hits this.

Verifying Lemon Squeezy webhook signatures (X-Signature)

A hex HMAC-SHA256 of the raw body, keyed with the signing secret you typed when creating the webhook. No timestamp, no prefix. Node, Python and Go code with a timing-safe compare, a curl test, and why "Invalid signature." usually means a parsed body.

Twilio: "Twilio Request Validation Failed" behind ngrok (X-Twilio-Signature and the URL)

X-Twilio-Signature is an HMAC-SHA1 of the webhook URL plus the sorted form fields, so the URL your app reconstructs behind ngrok (http://, localhost:3000, a trailing slash) fails with the right Auth Token. Each case checked with Twilio's own library, and the url option that fixes it in Express and Flask.