Your WhatsApp Webhook Is Verified and Still Receives Nothing
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.
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.
- Why does one callback URL not mean one working connection?
- How do you check which apps are subscribed to a WhatsApp Business Account?
- Which permissions does each webhook field require?
- Why can another company's app be subscribed to your customer's account?
- What does Meta do when your endpoint returns anything but 200?
- How large can a single webhook payload get?
- Which fields should you leave unsubscribed, and why?
- The notification arrived but the file did not - where did it go?
- How long does a media id from a webhook stay valid?
- Where should you start when nothing is arriving at all?
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.

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.

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:
- Callback URL and verify token - confirmed once, at setup, by Meta's verification
GET. If this ever fails, nothing past it matters yet. - The WABA is subscribed -
GET /{waba-id}/subscribed_apps, checked against your own app id, not assumed from the fact that setup once succeeded. - The field is subscribed at the app level - the Configuration panel, remembering this is global across every connected account, not per customer.
- The token holds the right permission -
whatsapp_business_messagingformessages,whatsapp_business_managementfor everything else, and advanced access if you are receiving on behalf of a customer rather than yourself. - The payload is under your own infrastructure's ceiling, which may be well below Meta's 3 MB, particularly for a
historyorsmb_app_state_syncevent. - Your endpoint returns 200 promptly, inside whichever retry window actually applies to your account.
- 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?
Why do I receive status updates but no inbound customer messages?
Can two apps receive the same WhatsApp webhook at once?
Does a failed webhook delivery lose the message permanently?
Should I download media on receipt or fetch it when a user opens the chat?
Why does my webhook work in testing and fail with real customer files?
- whatsapp webhooks
- whatsapp cloud api
- meta graph api
- webhook debugging
- developers