Confirm the hub.verify_token Meta sends matches WhatsAppCloudApi:VerifyTokenexactly (case-sensitive,
no surrounding whitespace).
Confirm the webhook URL registered in the Meta App Dashboard points at
https://{your-domain}/api/v1/notifications/webhook and is reachable over HTTPS from the public internet
(Meta will not call an unreachable or self-signed-cert URL).
Check GET Diagnostics/WhatsApp for a configuration warning if VerifyToken is empty.
Confirm WhatsAppCloudApi:AppSecret matches the Meta App's actual App Secret (not the access token, and
not the tenant's own WhatsAppOperationalSettings.AppSecret, which is a different value for a different
purpose — see §4.3).
Confirm nothing between Meta and the app (a reverse proxy, a WAF, a body-parsing middleware) rewrites or
re-encodes the request body — the signature is computed over the exact raw bytes Meta sent, and any
transformation invalidates it.
If testing manually via Postman, ensure the HMAC is computed over the literal JSON text of the request
body, not a re-serialized/pretty-printed version of it.
Meta long-lived access tokens eventually expire or can be revoked. A 401/400 from Meta on any send
surfaces as ErrorCode/ErrorMessage in the response, or as a Result<Guid>.FailureAsync message
containing Meta's error text for Communication-module sends.
GET Health reports isConfigured: true even for an expired token — configuration presence and token
validity are different checks. Use POST Test/WhatsApp or the smoke test to verify the token actually
works, not just that it's set.
As of the Phase 1 Runtime Consolidation, there is exactly one place this is configured:
WhatsAppCloudApi:GraphApiVersion (default v25.0). If a send fails with a Meta error referencing a
deprecated or unsupported API version, confirm this key rather than looking for a per-service override —
none exist anymore.
All phone normalization goes through WhatsAppPhoneHelper.TryNormalizePhone
(see §4, §12.5). Accepted input
shapes: 966XXXXXXXXX (already normalized), 0XXXXXXXXX (local), bare 5XXXXXXXX (9 digits), and any of
these with +/spaces/dashes. Anything else fails normalization and the dispatch is skipped before any
Meta call — check application logs for a "phone normalization failed" warning rather than assuming a Meta
rejection.
Confirm the template name is spelled exactly as approved in Meta Business Manager (case-sensitive) and is
approved for the specific languageCode being sent.
Confirm the number and type of components/parameters matches what the approved template expects — a
mismatch is a common cause of rejection surfaced via ErrorCode/ErrorMessage.
Use POST Diagnostics/Providers or POST Preview to see exactly which template name and language the
system resolved before the send was attempted.
Outbound media requires a publicly reachable URL (ImageUrl/DocumentUrl/VideoUrl) — Meta must be
able to fetch it directly; a URL behind the tenant's own auth wall will fail.
Inbound media download failures (GET Media/{messageId} returning 400) usually mean the underlying
DownloadMediaBytesAsync call to Meta failed — check whether the platform's AccessToken is still valid,
since expired media links on Meta's CDN also return errors independent of the token.
If a customer's display name is still missing after upgrading past library version 1.0.1, confirm the
incoming webhook payload actually includes a contacts array with a matching wa_id — some inbound
message types (e.g. system messages) may not include contact profile data at all, in which case null is
the correct, expected result, not a bug.
Confirm the ERP's Shumoul.WhatsAppIntegration.Core/.AbstractionsPackageReference versions are at
1.0.1 or later — the fix has no effect until the compiled package is actually referenced and deployed.
Confirm statuses[] entries are actually present in the webhook payloads Meta is sending — some
configurations only subscribe to the messages field, not message_status, in the Meta App Dashboard's
webhook subscription settings.
Check both consumers independently: WhatsAppInboxService.ProcessWebhookStatusAsync (Inbox UI) and
INotificationDeliveryReceiptService.UpsertAsync (Notification Framework) — a failure in one does not
block the other, since the controller processes each item independently (see
§8.8).