Features Pricing Use cases Compare Blog

Your WhatsApp Webhook Is Verified and Still Receives Nothing

16 min read

The short answer

Setting a callback URL on the app is only half the wiring. Each WhatsApp Business Account must be separately subscribed with a POST to the subscribed_apps edge, and the App Dashboard shows that subscription nowhere. Query the same edge with GET to confirm your own app id is listed. Missing permissions, oversized payloads and expired media ids account for most of what remains.

Outgoing webhook endpoint rows listing a callback URL, subscribed event names, a status column and delivery counts, with a control to reveal the signing secret

Why does one callback URL not mean one working connection?

Your endpoint answered Meta's verification GET request correctly. The App Dashboard shows the callback URL saved, the verify token matching, the fields you want ticked in the Configuration panel. By every signal your own screen gives you, the webhook is live. Customer messages still do not arrive.

This is the shape of the problem almost every other page on this query gets wrong, because it answers a question you have already ruled out. The tunnel is open. The endpoint returns 200. The messages field is ticked. None of that is what is missing.

The callback URL, the verify token and the field list are all properties of the app. A WhatsApp Business Account is a separate object, and Meta's reference for the subscribed_apps edge describes subscribing an app to a WABA's notifications as its own step, distinct from configuring the app itself (Meta - WhatsApp Business Account: Subscribed Apps). One app can be correctly configured and still be subscribed to zero accounts, one account, or several - and the dashboard screen that shows your callback URL does not show you which of those is true for any given WABA.

A callback URL that passes Meta's verification challenge tells you the app is wired correctly; it tells you nothing about whether this particular WhatsApp Business Account is subscribed to it. That is the gap every symptom in this post traces back to.

Seven gates in a single webhook delivery path, each labeled with the symptom it produces when it silently fails
One callback URL, several separate gates - and six of the seven failures look identical from outside

The diagram above is the shape of the whole problem: one delivery path, several gates, and most of the gates fail silently rather than with an error you can grep for. The rest of this post is those gates, in the order a message actually passes through them.

How do you check which apps are subscribed to a WhatsApp Business Account?

The object that answers this is the subscribed_apps edge on the WABA itself, not anything in the App Dashboard's Configuration panel. Meta's reference for it documents three operations: a POST that subscribes your app, optionally overriding the callback URL or verify token for that one account, a GET that lists who is currently subscribed, and a DELETE that removes a subscription (Meta - WhatsApp Business Account: Subscribed Apps).

POST   /{whatsapp-business-account-id}/subscribed_apps
GET    /{whatsapp-business-account-id}/subscribed_apps
DELETE /{whatsapp-business-account-id}/subscribed_apps

The GET response is a list, and each entry names an application - id, name, and where available a link to it. That list is the actual state of the connection. Nothing in the App Dashboard renders it; you have to call the edge yourself, with the token you intend to use for sending.

An Embedded Signup flow, a coexistence connect, and a manual credentials entry can each get this wrong in a different way, because only one of them - a completed signup - reliably calls subscribed_apps as part of finishing. WabaCRM's own connect flow treats the POST as necessary but not sufficient: it subscribes, then immediately re-reads the same edge with GET, and does not report the number healthy until its own app id appears in that list. A subscription call Meta accepted but that silently applied elsewhere looks identical to success from the POST response alone - the only way to know is to read the list back.

Checked against Meta's documentation on 4 September 2026.

An id that the GET response does not return at all - rather than returning with an unexpected value - is its own separate failure, closer in shape to what it means when a WhatsApp id resolves to nothing than to a subscription problem; that page is about an id Meta has stopped recognising, not one that was never subscribed.

Which permissions does each webhook field require?

Every webhook field needs one of exactly two permissions, and they are not interchangeable. Meta's documentation is specific about the split: whatsapp_business_messaging covers the messages field, and whatsapp_business_management covers every other field (Meta - WhatsApp Cloud API Webhooks).

Field (examples) Permission required
messages whatsapp_business_messaging
account_update, account_alerts whatsapp_business_management
message_template_status_update, message_template_quality_update whatsapp_business_management
phone_number_quality_update, business_capability_update whatsapp_business_management
smb_message_echoes, smb_app_state_sync whatsapp_business_management

The practical trap is that a token can hold one permission without the other, and everything upstream of the payload looks fine regardless - the field is ticked, the WABA is subscribed, the callback URL answers. What arrives is governed by which scope the customer granted or the platform token carries, invisible until a specific field simply never shows up in your log.

Meta's guide adds that a partner - anyone receiving these webhooks on behalf of onboarded business customers rather than for their own single account - needs advanced access to these permissions through App Review before a customer's grant is enough on its own (Meta - Set up webhooks). Standard Access can carry you through everything that involves a role on your own app; it will not carry an arbitrary customer's account, and the failure at that boundary is a permission gap, not a webhook misconfiguration, even though the symptom - nothing arrives - is the same one this whole post is about. It is also a different failure from an account Meta has actually restricted, which produces its own distinct symptoms - see what a WhatsApp account restriction actually looks like before assuming a missing scope and a blocked account are the same outage.

Checked against Meta's documentation on 4 September 2026.

Why can another company's app be subscribed to your customer's account?

Because subscribed_apps is additive, not exclusive. Nothing in Meta's documentation for the edge describes subscribing one app as removing another, and a GET on a WABA that has changed hands - moved between BSPs, migrated off a previous tech provider, or partly onboarded twice - can and does list more than one entry.

This matters most for a number arriving through coexistence, where the WhatsApp Business app on the customer's own phone and a Cloud API integration share one number, and for any account switching onto your platform from somewhere else. A prior integration's app can still be subscribed weeks after the customer believes they have left it, quietly receiving the same inbound messages your system is also trying to receive - visible only by reading the list, never by anything failing on your side.

The distinction between a BSP-shaped relationship and a Tech Provider one is relevant here for a specific reason: a Tech Provider has no standing to remove another company's subscription from a customer's WABA on the customer's behalf, because the account is never the provider's asset to begin with - see BSP vs Tech Provider for what that distinction actually controls. The customer, or whoever still holds a system-user token with management permission on that WABA, is the one who can call subscribed_apps to remove an entry. Your own job is narrower: confirm your app's id is present, and treat any other id in the same list as a fact about the account's history rather than a bug in your integration.

A GET on subscribed_apps that returns more than one application is not itself an error - it is evidence you need to read, naming exactly who else is receiving this customer's messages right now.

What does Meta do when your endpoint returns anything but 200?

It retries, but the two Meta pages that describe retry behaviour for this exact scenario do not agree with each other on how long.

The Graph API's general webhooks documentation says Meta will "retry immediately, then try a few more times with decreasing frequency over the next 36 hours" before an unacknowledged notification is discarded (Meta - Graph API Webhooks: Getting Started). The WhatsApp-specific webhooks page states its own figure: Meta "retries delivery with decreasing frequency until the request succeeds, for up to 7 days" (Meta - WhatsApp Cloud API Webhooks). Thirty-six hours and seven days are not the same claim, and nothing in either document reconciles them - one may be the general Graph API default and the other a WhatsApp-specific override, but neither page says so, and this post is not going to guess on your behalf which one governs your account.

What both pages agree on is narrower and more useful: retries happen with decreasing frequency, your endpoint should return 200 OK to stop them, and a retried notification can duplicate - so deduplicate on the message id in your own handler rather than assuming exactly-once delivery.

What neither page documents, and what this post will not assert despite it circulating as received wisdom in more than one internal engineering note we have seen: that Meta automatically disables a subscription after a run of failures. It may be true. It is not written down anywhere Meta publishes, and the honest position is that sustained failure costs you retries for the stated window and duplicate deliveries to every other app still subscribed to the same account - not a confirmed auto-disable event you can plan an alert around.

Checked against Meta's documentation on 4 September 2026.

How large can a single webhook payload get?

Meta's own ceiling is stated plainly: "Webhook payloads can be up to 3 MB" (Meta - WhatsApp Cloud API Webhooks). The general Graph API webhooks documentation adds a second, unrelated ceiling on top of that one - event notifications are batched, "aggregated and sent in a batch with a maximum of 1000 updates," with an explicit warning that batching cannot be relied on and your server should still handle each entry independently (Meta - Graph API Webhooks: Getting Started).

Three megabytes is generous for a text message and unremarkable for a batch of delivery receipts after a broadcast, but it is not generous against whatever your own infrastructure accepts before the request reaches your application code. A reverse proxy's body-size limit, a PHP post_max_size, or a database driver's packet ceiling can all sit well under 3 MB, and each rejects the request before your handler ever runs - a connection reset or a bare 4xx from infrastructure that has nothing to do with WhatsApp, on a payload Meta considers entirely ordinary.

This is worth checking against your own stack's numbers specifically, not against Meta's number alone - a webhook receiver sized for a typical single-message payload can be an order of magnitude under 3 MB in practice, and the gap only shows up the day a payload happens to be large.

Checked against Meta's documentation on 4 September 2026.

Which fields should you leave unsubscribed, and why?

The 3 MB ceiling from the previous section is not evenly distributed across fields. Two in particular are documented as carrying far more data per event than the rest: history, which synchronises chat history for partner-onboarded customers, and smb_app_state_sync, which synchronises contacts for WhatsApp Business app users (Meta - WhatsApp Cloud API Webhooks). Both exist for coexistence-style onboarding, where a number that already has message history on the customer's phone needs that history reflected into a new system.

Field subscription is app-global rather than per-WABA - ticking one of these on turns it on for every account connected to the app, not just the one you meant to backfill. A payload near the 3 MB ceiling that your infrastructure was never sized for does not fail quietly; it fails loudly, for every customer, the moment the first large backfill event arrives. If you store raw payloads for audit or replay, the write path needs headroom for a history event specifically, not the ordinary size of a messages event. This is the same field pair coexistence-related webhook limits already warns about from the account-configuration side; this piece is the delivery-pipeline side of the same body-size ceiling.

The pragmatic default is to leave both fields unsubscribed unless a specific feature depends on the backfill, and to raise your own body-size and packet ceilings deliberately before ever ticking either one - not after the first oversized payload arrives and gets refused.

The notification arrived but the file did not - where did it go?

A webhook telling you a customer sent a photo is not the photo. The messages entry for a media type carries a media object - an id, a mime type, a file size, a checksum - and none of those fields are the bytes themselves. Retrieving the actual file is a second, separate call: GET /{media-id} against the Graph API, which returns a download URL you then fetch independently (Meta - WhatsApp Cloud API: Media).

That download URL is short-lived - Meta states plainly that "Media URLs expire after 5 minutes" - so a handler that resolves the URL and then queues the actual download for later, rather than fetching it inline, can lose the file to nothing more dramatic than an ordinary queue delay. Nothing in that failure looks like an error at the point it happens; the id resolved successfully, the URL was valid at the moment it was issued, and the download simply started too late against a URL that was never meant to be held.

This is precisely the kind of failure that a message logs screen is built to surface, because it happens downstream of the webhook itself and produces no error at the point Meta is involved at all - only a status and a reason on your own side, on a message that otherwise arrived correctly. It is a different class of gap from the one covered in why WhatsApp sometimes reports a message type as unsupported, where the content never reaches you at all rather than arriving and then failing to download - worth ruling out first, since the two look similar from an agent's chair.

A message logs table showing timestamped rows with recipient, delivery status and a failure reason column, above date, number and status filters
Failure reasons live in the message log, not in the webhook payload that preceded them

How long does a media id from a webhook stay valid?

Two different lifetimes apply to what looks like the same kind of id, and confusing them is the second half of the failure above. A media id your own app mints by uploading a file - the kind drip campaigns reuse from the media library across a running sequence - is valid for 30 days: "Media IDs returned by the API expire after 30 days" (Meta - WhatsApp Cloud API: Media). A media id that arrives inside an inbound webhook is a different case with a shorter clock: "Media IDs in webhooks expire after 7 days," per the same page.

A media id received in a webhook is valid for 7 days, not the 30 days that applies to an id your own app generated by uploading a file - conflating the two is what makes a delayed download fail weeks after everyone has stopped looking at that conversation.

The practical consequence is about when you download, not how. Fetch inbound media as close to receipt as your pipeline allows, and store the resulting file on infrastructure you control rather than treating Meta's copy as a backing store you can return to later - even seven days feels generous until a customer's file sits in a retry queue, a moderation review, or an unrelated backlog long enough to outlast it.

Checked against Meta's documentation on 4 September 2026.

Where should you start when nothing is arriving at all?

In the order a message actually has to clear each gate, which is also the order the diagram above lays out:

  1. Callback URL and verify token - confirmed once, at setup, by Meta's verification GET. If this ever fails, nothing past it matters yet.
  2. The WABA is subscribed - GET /{waba-id}/subscribed_apps, checked against your own app id, not assumed from the fact that setup once succeeded.
  3. The field is subscribed at the app level - the Configuration panel, remembering this is global across every connected account, not per customer.
  4. The token holds the right permission - whatsapp_business_messaging for messages, whatsapp_business_management for everything else, and advanced access if you are receiving on behalf of a customer rather than yourself.
  5. The payload is under your own infrastructure's ceiling, which may be well below Meta's 3 MB, particularly for a history or smb_app_state_sync event.
  6. Your endpoint returns 200 promptly, inside whichever retry window actually applies to your account.
  7. Media is fetched inside its window - 7 days for a webhook-received id, not the 30 you may be used to from your own uploads.

Six of these seven fail with no error message a human reads. The dashboard shows a saved callback URL whether or not any WABA is subscribed to it. A permission gap and an unsubscribed WABA both produce exactly the same absence. The one exception is the payload ceiling, which at least tends to fail loudly, on infrastructure that has nothing to do with WhatsApp - and even that one is easy to blame on the wrong layer.

None of this is a criticism of Meta's documentation, which states every one of these behaviours plainly, each on its own page. The failure mode of this whole class of bug is that verification succeeding answers a question you had already stopped asking, and the actual gate is one page over. Read subscribed_apps back with a GET before anything else, on the account that is not receiving messages, using the token you actually intend to send with - it is the one check that directly answers whether your app, specifically, is the one Meta is trying to notify. Once the API side checks out, a shared inbox built to show that arrival to more than one agent is a reasonable place to confirm the message actually landed.

Every Meta quotation on this page was read from Meta's own documentation on 4 September 2026. Meta changes that documentation without notice; the linked pages are authoritative and this one is not.

Questions people also ask

Do I need to subscribe every WhatsApp Business Account separately?

Yes. The callback URL, verify token and field list live on the app, but delivery to any given WABA depends on a separate POST to that WABA's subscribed_apps edge. Meta's own reference documents this as its own operation, distinct from the App Dashboard's Configuration panel, which only shows what the app is willing to send, not which accounts are actually wired to it. A WABA created before your app existed, or moved in from another integration, will not be subscribed automatically. Query GET on the same edge for that WABA and confirm your app's id is in the returned list before assuming the connection works.

Why do I receive status updates but no inbound customer messages?

Status updates and inbound messages both arrive on the messages field, so if you are getting one you are usually getting both from Meta's side - the split is more often downstream, in your own handler. Check whether your code silently drops entries with no messages key while processing statuses, or whether a permission gap on whatsapp_business_messaging (as opposed to whatsapp_business_management, which covers most other fields) is scoped differently for your token than you expect. Confirm in your raw request log, before any parsing, that the messages array is genuinely absent rather than present and discarded.

Can two apps receive the same WhatsApp webhook at once?

Yes - the subscribed_apps edge's GET response is a list, and the schema allows more than one entry, so more than one app can be subscribed to the same account at once. This is the normal state during a migration between providers, or whenever a previous integration was never unsubscribed, and it explains why a WABA can look healthy in your dashboard while another company's system is also receiving the same customer messages. Read the GET response as a list, not a single value, and check for entries you did not expect rather than assuming your own app is the only one present.

Does a failed webhook delivery lose the message permanently?

Not immediately. Meta's Graph API webhook documentation describes retrying a failed delivery with decreasing frequency for a stated window before giving up, and the WhatsApp-specific webhook page independently states its own retry period, which is considerably longer. The two pages do not agree with each other on the exact duration, and neither documents an automatic unsubscribe once retries end. In practice this means an endpoint that is down for an hour usually recovers with no data loss, but one that stays down past the retry window loses the notification outright, with nothing further attempted.

Should I download media on receipt or fetch it when a user opens the chat?

Download on receipt. The webhook payload never contains the file itself, only a media id, and that id has its own expiry that is shorter than the id your own app mints when it uploads media. Deferring the download until a user opens the conversation means the fetch can fail hours or days later for a reason that has nothing to do with that moment - the id simply aged out first. Retrieve the file as soon as the notification arrives and store it on your own infrastructure; treat Meta's copy as temporary by design, not as a backing store.

Why does my webhook work in testing and fail with real customer files?

Test messages are usually small and sent in isolation, so they stay well under any payload ceiling and finish downloading before any id expires. A real customer's file can be large enough on its own, or arrive as part of a burst that queues your download behind other work, so the same code path that handled a 40 KB test image can fail on a 12 MB voice note or on a file whose processing was delayed by minutes rather than seconds. Reproduce with a file close to your actual size ceiling and with an artificial delay before the download step, not with a clean, immediate test payload.

Keep reading

Your customers are already on WhatsApp

Free for your first 1,000 contacts, with no time limit and no card. Setting up the workspace takes minutes; connecting a number takes as long as Meta's own review of it.

Sign up with your company email address. No sales call, no onboarding fee, nothing to schedule.

Why this is safe to point your customer list at

Payments are processed by Razorpay on their own checkout — your card details are never entered on, or stored by, WabaCRM. Every inbound WhatsApp webhook is checked against its signature before it is trusted.

Tech Provider is a Meta platform access tier — not a partnership, a reseller agreement or an endorsement.