MeatButton

Webhook Not Working? Invalid Payload and Signature Verification Failed

For anyone whose webhook integration isn't receiving or processing events

Something happened on a service you use — a payment came through, a form was submitted, a message was sent — and your app was supposed to react to it. But it didn't. You dig into your logs and find errors like:

Or maybe the opposite: no errors at all, because the webhook never arrived in the first place. Either way, your app isn't reacting to events it should know about.

Here's what's actually going on.

What a webhook is, in plain English

Normally when your app needs information from another service, it asks for it. Your app calls the service's API and says "give me the latest data." That's a pull.

A webhook is the opposite. It's a push. Instead of your app asking, the other service calls YOUR server when something happens. Stripe calls you when a payment goes through. GitHub calls you when someone pushes code. Shopify calls you when an order is placed. Twilio calls you when a text message arrives.

You give the service a URL on your server — something like yourapp.com/api/webhooks/stripe — and when the event happens, the service sends an HTTP request to that URL with all the details of what just happened. Your app receives it, reads it, and does the right thing.

Simple idea. Lots of ways it breaks.

The signature problem (where most errors come from)

Here's a question: if anyone on the internet can send a request to your webhook URL, how does your app know the request actually came from Stripe (or GitHub, or whoever) and not some random person pretending to be them?

The answer is signatures. It works like this:

  1. When you set up the webhook, the service gives you a secret key. You store this in your app.
  2. When the service sends a webhook, it takes the entire message body, runs it through a cryptographic function along with that secret, and attaches the result as a signature in the request headers.
  3. Your app receives the request. It takes the message body, runs the same cryptographic function with the same secret, and compares its result to the signature that came with the request.
  4. If they match, the message is genuine. If they don't, someone is either faking the webhook or something changed the message in transit.

This is why "signature verification failed" is such a common error. If anything goes wrong with this process — wrong secret, modified body, different encoding — the signatures won't match, and your app rejects the webhook.

The most common causes

1. Wrong webhook secret

This is the number one cause. You have a secret stored in your app, but it doesn't match the one the service is using to sign the webhooks.

How does this happen?

Fix: Go to the service's dashboard, find the webhook endpoint, and copy the signing secret directly. Paste it into your environment variable. Make sure there are no trailing spaces, no line breaks, no extra quotes around it. Redeploy.

2. Your middleware is modifying the request body

This one is sneaky and extremely common. Here's what happens:

The service sends the webhook with a raw JSON body. Before your webhook handler sees it, a middleware in your app (like a JSON body parser) reads the body, parses it into a JavaScript object, and then when your code tries to verify the signature, it re-serializes it back to a string. But the re-serialized version isn't byte-for-byte identical to what was originally sent. Maybe the key order changed. Maybe whitespace was stripped. The signature check fails.

In Express.js, this is the classic problem:

// This breaks webhook verification:
app.use(express.json());  // parses body BEFORE your webhook route

// This works:
app.post('/webhook', express.raw({type: 'application/json'}), handler);
// Get the raw body FIRST, verify, THEN parse

The signature was computed against the raw bytes the service sent. If your app doesn't verify against those exact same raw bytes, the check will fail every time.

Fix: Make sure your webhook endpoint receives the raw, unparsed request body for signature verification. Every service's SDK documentation explains how to do this for your specific framework. Read that page, not a random tutorial.

3. Wrong endpoint URL

The webhook is configured to send to the wrong address. Maybe it's pointing at localhost from development. Maybe there's a typo. Maybe you moved your app to a new domain and didn't update the webhook URL. The service tries to deliver the webhook, gets a connection error or a 404, and your app never sees it.

Signs: You see no incoming requests at all in your server logs. The service's dashboard shows delivery failures with connection errors or 404s.

Fix: Check the webhook URL in the service's dashboard. It needs to be your live, deployed, publicly-accessible URL. Not localhost. Not an internal IP. Not a URL behind a firewall.

4. Your server is returning errors, causing retries

Your app receives the webhook, tries to process it, and crashes. It returns a 500 error. The service sees the failure and thinks "delivery failed, I'll try again." It sends the same webhook again. Your app crashes again. The service tries again. And again.

Now you have a different problem: when you finally fix the crash, all those retried webhooks arrive and your app processes the same event five times. The customer gets five confirmation emails. Five records are created in the database. Five charges might get applied.

Fix: Two things. First, fix whatever is causing the 500 error — check your server logs for the actual stack trace. Second, make your webhook handler idempotent. That means if it receives the same event twice, it only processes it once. Most services include a unique event ID in the webhook payload. Store it, check for duplicates before processing.

5. Your server is too slow to respond

Most services expect your webhook endpoint to respond within 5 to 30 seconds. If your handler does a bunch of slow work — sending emails, calling other APIs, running database queries — it might not respond in time. The service times out, marks the delivery as failed, and retries.

Fix: Your webhook handler should do the minimum: receive the payload, verify the signature, store the event, and immediately return a 200 response. Do the heavy processing afterward, in the background. Acknowledge fast, process later.

How to test and debug webhooks

Check the service's delivery logs

Almost every service that sends webhooks has a dashboard showing delivery attempts. Stripe, GitHub, Shopify, Twilio — they all have this. Go there first. You'll see whether the webhook was sent, what response your server returned, and the full request body. This tells you instantly whether the problem is on the sending side or the receiving side.

Use the service's test feature

Most services let you resend a webhook or send a test event from their dashboard. Use it. It lets you trigger a webhook on demand without waiting for a real event to happen. Watch your server logs while you do it.

Inspect the raw request

If you want to see exactly what's being sent to your endpoint, use a tool like webhook.site. It gives you a temporary URL that captures any request sent to it. Point the service's webhook at that URL temporarily, trigger a test event, and you can inspect the headers, body, and signature. This rules out "is the service sending the right thing?" as a question.

Check your environment variables

Log into your hosting platform and verify that the webhook secret environment variable is set, has the right value, and is available to your app. A surprising number of webhook failures come down to a missing or incorrect env var that nobody noticed.

Why AI struggles with webhook problems

Webhook debugging requires seeing both sides of the conversation: what the service sent and what your server received. An AI can look at your code and tell you whether your verification logic seems right. But it can't see the actual incoming request. It can't check the service's delivery logs. It can't tell you whether your environment variable has the right value or whether your hosting platform is stripping a header.

The AI will look at your code, see that it follows the standard pattern, and tell you it looks correct. And the code probably IS correct. The problem is usually configuration: a wrong secret, a middleware that runs before your handler, a URL that points to the wrong place. These aren't code problems. They're environment problems. And the AI can't see your environment.

A human can SSH into your server, tail the logs, see the incoming request, compare it to the service's delivery log, and find the mismatch in minutes. That's the kind of problem where a second pair of eyes — real eyes — saves you hours.

Webhooks failing and you've tried everything?

Webhook problems live in the gap between your code and your environment — exactly where AI can't see. Press the MeatButton and a real expert will look at both sides of the problem: what's being sent and what's being received. First one's free.

Get MeatButton