WhatsApp Webhook Not Working? Causes and Fixes
You've sent a test message to your business number three times. The chat shows it delivered. Your server log shows nothing. Or the dashboard refuses to save your Callback URL with a vague "couldn't be validated". Or events arrive, but every one of them gets a 401 from your own code.
When a WhatsApp webhook is not working, the cause almost always falls into one of three buckets, and the symptom tells you which: verification fails, verification passes but nothing arrives, or events arrive and your handler rejects or mishandles them.
This guide goes bucket by bucket with the specific causes we see most, how to confirm each, and curl commands to test your endpoint without waiting on Meta. It covers Meta's Cloud API first, then Tenashi's webhooks. For background, see the complete guide to the WhatsApp API.
First, prove your endpoint works in isolation
Before touching Meta's settings, check your endpoint does what Meta expects. This simulates the verification request Meta sends when you save the Callback URL:
curl -i "https://your-domain.example/webhooks/whatsapp?hub.mode=subscribe&hub.verify_token=$VERIFY_TOKEN&hub.challenge=1158201444"
You should get HTTP/1.1 200 (or HTTP/2 200) and a body of exactly 1158201444. Then confirm the endpoint accepts a signed POST and rejects an unsigned one:
URL="https://your-domain.example/webhooks/whatsapp"
BODY='{"object":"whatsapp_business_account","entry":[]}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$APP_SECRET" | sed 's/^.* //')
curl -s -o /dev/null -w "signed: %{http_code}\n" -X POST "$URL" \
-H "Content-Type: application/json" -H "X-Hub-Signature-256: sha256=$SIG" --data-binary "$BODY"
curl -s -o /dev/null -w "unsigned: %{http_code}\n" -X POST "$URL" \
-H "Content-Type: application/json" --data-binary "$BODY"
Expect 200 then 401 (or another 4xx). If both pass, the problem is in Meta's configuration, not your code. If you aren't sure whether Meta is sending at all, Meta also documents a throwaway test endpoint that just logs payloads; if that receives events and yours doesn't, the issue is in your code.
Bucket 1: verification fails when you save the Callback URL
Meta marks the endpoint verified only if it gets HTTP 200 with the hub.challenge value in the body. Anything else and no webhooks are ever sent. The usual reasons:
- The body isn't the bare challenge. Returning JSON (
{"challenge": "1158201444"}), a quoted string, or an HTML page from a framework's default handler all fail. Return the value as plain text. - The verify token doesn't match. Trailing whitespace or a newline in an environment variable, a different value in staging, or comparing against
undefinedbecause the variable never loaded. Log both values (in a safe environment) and compare. - The query parameters never reach your code. PHP converts dots in query parameter names to underscores, so
hub.modearrives as$_GET['hub_mode']. Some routers also drop or rename unusual parameters. - TLS problems. Meta requires HTTPS with a valid, correctly installed certificate and rejects self-signed ones. A missing intermediate certificate works in your browser but can fail elsewhere; run your domain through an SSL checker.
- The URL isn't publicly reachable.
localhost, a VPN-only host, basic auth, a login wall, or a hosting provider's deployment protection all block Meta. Use a tunnel for local development. - A redirect. An
httptohttpshop, a trailing-slash redirect or awwwredirect returns 301 or 308, not the 200 Meta wants. Enter the final URL exactly.
Bucket 2: verified, but no events arrive
This is the most frustrating bucket, because the dashboard looks correct. Work through these in order:
- The field isn't subscribed. Verification only unlocks the field list. In App Dashboard, WhatsApp, Configuration (or Use cases, Customize, Configuration for apps created with the "Connect with customers through WhatsApp" use case), make sure
messagesis subscribed. - Your app isn't subscribed to the WhatsApp Business Account. A verified Callback URL on the app is not enough; the account that owns the number must have your app subscribed. The check and the fix are below this list.
- The app is in Development mode. Meta's troubleshooting note is explicit: some webhooks are not sent while the app is in Dev mode. Switch it to Live.
- An override is sending events elsewhere. Meta supports an alternate callback URL per WhatsApp Business Account and per phone number, and checks the number first. A forgotten override from a staging setup quietly diverts
messagesfor that number. Remember that template and account fields ignore overrides, so "template webhooks arrive but messages don't" points straight at one. - You're messaging a different number. Teams with a test number and a production number often send to one while the app is subscribed to the other's account.
- A firewall or bot filter blocks Meta. WAF rules, rate limiters and bot challenges can block or challenge Meta's
POSTs. If you allowlist Meta's IPs, note that Meta changes them periodically; it suggests mutual TLS as the more durable option. - The token lacks permissions. Receiving
messageswebhooks needswhatsapp_business_messaging; other fields needwhatsapp_business_management.
To check cause 2, list the apps subscribed to the account, and subscribe yours if it's missing:
curl "https://graph.facebook.com/v23.0/WABA_ID/subscribed_apps" \
-H "Authorization: Bearer $WHATSAPP_TOKEN"
curl -X POST "https://graph.facebook.com/v23.0/WABA_ID/subscribed_apps" \
-H "Authorization: Bearer $WHATSAPP_TOKEN"
If your app isn't in the data array the first call returns, that's the problem; the second call adds it and returns "success": true. Use the current Graph API version from Meta's changelog in place of v23.0.
Our guide to WhatsApp API webhooks covers fields and overrides in more detail.
Bucket 3: events arrive, but your handler drops them
Here Meta is doing its job and your code isn't. These show up as 4xx or 5xx responses in your logs, or as the same event arriving again and again.
- Signature mismatch on every request. Your framework parsed the JSON before you hashed it, or you're using the wrong secret (the app secret signs webhooks; the verify token and access token don't). The fix and per-framework code are in verifying WhatsApp webhook signatures.
- Request too large. Meta payloads can be up to 3 MB, and Express body parsers default to 100 KB. Large batches then fail with 413, look like random drops, and get retried.
- The handler assumes the shape. Code that reads
value.messages[0]crashes on a status update (which hasstatusesinstead), an error notification (value.errors) or a batched payload. Each crash returns 500, Meta retries, and the queue of failing events grows. Loop over every entry and change, and check which arrays exist. - The handler is slow. If you call an AI model or a CRM before responding, deliveries time out and get retried, producing duplicates. Meta asks for a median response time of 250 ms or less, with under 1% above a second. Store the payload, return 200, then process.
- No de-duplication. Meta retries failed deliveries for up to 7 days and sends retries to every app subscribed to the account. Without a unique key on the
wamid, a single bad hour can replay into duplicate tickets or double replies.
After any of these is fixed, Meta's retries mean events that failed in the meantime can still arrive, so a healthy endpoint often catches up on its own.
Use Meta's tools before writing more code
Two built-in tools save time. App Dashboard, WhatsApp, Configuration can send a test payload for a field to your endpoint, which proves the path from Meta to your server without needing a real message. And because Meta provides no API for fetching past webhook data, log the raw body and headers of every request you receive (with customer content redacted or access-controlled) so you can replay real payloads against a fixed handler. If you're still setting things up, our Cloud API setup guide covers the dashboard steps in order.
When Tenashi webhooks aren't arriving
Tenashi's webhooks are simpler to debug because the history is in the product. Settings shows the last 100 deliveries for each endpoint, with the response code, a Resend button for events up to 7 days old, and Send test. The rules that most often explain a failure:
- Reply with any 2xx within 10 seconds. Do slow work after responding.
- Don't redirect. Tenashi's docs count redirects as failures, so register the final HTTPS URL.
- Failed deliveries are retried 8 more times over about a day. After 3 days of failures the endpoint is switched off; turn it back on in Settings once fixed.
410 Gonestops delivery on purpose. If an old endpoint returns 410, Tenashi treats it as unsubscribed.- Sign the right string. Tenashi hashes the
X-Tenashi-Timestampvalue, then a dot, then the untouched body, using thewhsec_secret of that specific endpoint, not another one.
Also check that the number itself is connected. Events stop when a number disconnects, and a number.disconnected event tells you when that happens.
Where Tenashi fits
A lot of webhook debugging on Meta's side is configuration: apps, accounts, fields, modes and overrides that all have to line up. If what you need is signed events when customers message your team, Tenashi has one place to add an endpoint, a delivery log with resend, and events for DMs and groups on the number you already use, linked by QR scan. The API is on every paid plan, from $18/mo.
Tenashi isn't Meta's Cloud API, so it won't help with template or account webhooks, or with high-volume automated messaging.
Read the API docs · Get started
FAQ
Why does Meta say my WhatsApp webhook callback URL couldn't be validated?
Meta needs an HTTP 200 whose body is exactly the hub.challenge value, and only when hub.verify_token matches. Returning JSON, a redirect, an error page or a mismatched token fails. Also check the URL is public HTTPS with a valid, non-self-signed certificate. Test with curl before saving the URL in the dashboard.
My webhook is verified but I'm not receiving messages. Why?
Usually one of three things. The messages field isn't subscribed, your app isn't subscribed to the WhatsApp Business Account that owns the number (fix with POST to the subscribed_apps endpoint), or the app is still in Development mode, where Meta doesn't send some webhooks. Check for callback overrides on the account or number too.
Why do I keep receiving the same webhook again and again?
Meta retries any delivery that didn't get a 200, with decreasing frequency for up to 7 days. If your handler crashes on certain payloads or responds too slowly, the same events keep coming back. Return 200 quickly after storing the payload, process asynchronously, and de-duplicate on the message wamid.
Do I need to allowlist Meta's IP addresses for webhooks?
No, and it can cause outages. Meta publishes its ranges but changes them periodically, so a stale allowlist silently blocks real events. Verify the X-Hub-Signature-256 header on every request instead, and add mutual TLS if you need network-level assurance, which is the option Meta itself suggests.
Why is my WhatsApp webhook returning 401 or 403?
If it's your own endpoint returning 401, your signature check is failing, almost always because the body was parsed before hashing or the wrong secret is used. A 403 often comes from a firewall, bot protection or auth middleware in front of your code. Check which layer produced the response.
Most webhook failures are one misaligned setting or one assumption in the handler. Want a delivery log and a resend button out of the box? Tenashi's webhook docs cover setup in a few minutes.