Push notifications aren’t arriving

Goal

You’ve got push notifications configured, but a device — or every device — isn’t receiving them. This article walks you from “nothing arrives” to a specific cause, following the order failures actually happen along the delivery path: credentials → device token → opt-out/permission state → channel routing → cron queue → platform-specific delivery.

Here’s the path a push takes on its way out of WordPress:

  1. Something fires mam_notification_send_message (or mam_notification_send_pn directly).
  2. The notification dispatcher resolves the notification type and decides which channels (email / SMS / push) are enabled.
  3. For push, it either dispatches immediately or queues the message for cron, depending on batch size and the global cron setting.
  4. MAM_PushDispatcher reads the recipient’s device token(s) from user meta and routes to the iOS (APNs), iOS VoIP, or Android (FCM) sender.
  5. The platform sender authenticates with stored credentials and posts to APNs / FCM.

A break at any step looks identical from the phone: nothing shows up. So work the steps below in order — each one rules out a layer of the path.


Prerequisites

  • Admin access to Mobile App Manager in WordPress.
  • A test device with the app installed and a user signed in on that device (tokens are tied to a signed-in WordPress user — see Step 2).
  • Ability to view WordPress user meta (a tool like a user-meta editor, or developer access to the database).
  • For credential checks: access to your APNs .p8 auth key details (Key ID, Team ID, app Bundle ID) and your Firebase service-account JSON.

The tokens and opt-out flags described here live per-user in WordPress user meta; your APNs/FCM credentials live in site options plus the key files uploaded to the server. The platform senders (MAM_IosCredentials / MAM_AndroidCredentials) read all of it at send time.


Steps

1. Confirm the send is actually firing

Before you go chasing credentials, first prove the push is actually being attempted.

  • Use the admin “Send Test Message” notification button (backed by the mam_main_send_test_not AJAX handler). This fires mam_notification_send_message for a single recipient with send_now = true, which is the cleanest end-to-end test of one user’s full push path.
  • Pick a test recipient who is currently signed in on a real device — not yourself in a browser.

If the test send reports success in the admin UI but nothing arrives on the device, the message did leave WordPress — so the problem lives downstream, in the token, opt-out state, credentials, or platform. Move on to the next step.

If the test send doesn’t even register, the notification type may have push disabled. Check the per-notification push toggle for that notification type (stored as tsl_notification_pn_status_{slug}). The dispatcher only routes to the push channel when that option is set or the notification type lists push in its default_on set.

2. Verify the device has a registered token

This is the most common cause of all. The mobile app writes its push token into the signed-in user’s WordPress user meta on launch / login. No token, no push — the dispatcher silently skips a recipient who has none.

Check these user-meta keys on the test recipient:

Meta key Platform Notes
mam_app_ios_pn_token iOS Standard APNs token
mam_app_android_pn_token Android FCM token
mam_ios_voip_token iOS VoIP Separate token for incoming-call wakeups; see Step 7
push_enabled both User-side opt-in flag
push_status both Last reported OS permission state

Common findings:

  • No token at all. The app never registered. Make sure the user actually signed in inside the app (tokens arrive via the app’s login/registration call carrying deviceios / deviceand, not via a web login). Have them fully sign out and back in on the device.
  • A stale token. APNs and FCM tokens change when a user reinstalls the app or restores from a device backup. The old token will fail at delivery (APNs returns BadDeviceToken). A fresh sign-in rewrites the token.
  • You’re testing the wrong user. Tokens are per-user. Confirm the device’s signed-in WordPress user is the recipient you’re sending to.

3. Check opt-out and OS permission state

Even with a valid token, the user may have turned push off — at the app level or the OS level.

  • push_enabled is the user-side opt-in flag. If it’s 0, the user opted out in-app.
  • push_status records the last OS permission state the app reported, via the status-update AJAX handler. The allowed values are: authorized, provisional, ephemeral, denied, not_determined, unknown. A value of denied means the user declined notifications at the iOS/Android system prompt — no credential or server fix will deliver a visible push until they re-enable it in device Settings.
  • push_status_updated is the timestamp of the last report — useful for telling whether the app has checked in recently at all.

If push_status reads denied, stop here — the fix lives on the device (re-granting notification permission in OS settings), not on the server.

4. Confirm channel routing is enabled for this notification type

The dispatcher decides, notification type by notification type, whether push is switched on. For a given notification slug, push is sent only when:

  • the option tsl_notification_pn_status_{slug} is set, or
  • the notification type declares push in its default_on list (registered via the mam_notification_list filter).

If email gets through but push doesn’t for the same notification, this routing toggle is almost always the culprit: the push channel is simply switched off for that type, even with email switched on. Re-check the toggle in the Notifications Manager settings for that notification.

5. Check the cron queue (high-volume sends)

For small sends, the dispatcher fires push immediately; for larger sends, it queues the send to cron instead. Here’s what decides which path a send takes:

Push is queued when the batch has more than 10 messages and the global option mam_notification_use_cron is enabled. Otherwise it dispatches immediately.

So a single test send always goes out immediately, while a broadcast to your whole user base goes through the queue instead. If broadcasts “don’t arrive” while test sends do, the queue isn’t draining:

  • Queued push rows are written to the push/text queue table (the wp_mam_pn_text_queue table, shared between SMS and push) via add_to_push_notifications().
  • A MAM cron job drains that queue and fires mam_notification_send_pn for each row.
  • Check Mobile App Mgr → Scheduled Jobs (the cron list). Each registered job shows its Last Run and Next Run. If the queue-draining job’s Last Run is stale, WordPress cron isn’t firing.

WordPress cron only fires when the site gets traffic (or a real server cron pings wp-cron.php). On a low-traffic site, queued pushes can sit until the next page load. If Last Run is hours old, set up a server-level cron to hit wp-cron.php on a schedule, or temporarily disable cron queueing for notifications (turn off mam_notification_use_cron) so sends go out immediately.

6. Validate platform credentials — iOS (APNs)

If the message is firing, the token’s valid, and routing is switched on, but still nothing lands, the platform sender itself is either failing to authenticate or getting rejected by APNs.

iOS uses APNs over HTTP/2 with a provider auth token signed from your .p8 key. Confirm these are correct and consistent:

  • APNs .p8 auth key (Key ID + key contents) — loaded by MAM_IosCredentials.
  • Team ID (ios_pn_team_id).
  • App Bundle ID (ios_pn_app_bundle_id) — must exactly match the app’s real bundle identifier.

Common APNs failures:

  • Wrong Bundle ID / Team ID → APNs rejects every send for the app.
  • Sandbox vs production environment. A development (Xcode/TestFlight-from-Xcode) build registers a sandbox APNs token, while store builds register production tokens. The sender tries the production APNs host first and automatically retries sandbox when production rejects the token, so a pure environment mismatch is usually recovered on the second attempt — but it surfaces as production BadDeviceToken errors in the ios_pn debug log and adds latency. Persistent non-delivery here usually means the token is genuinely invalid, not merely the wrong environment.
  • BadDeviceToken in the per-token results means the token is stale or from the wrong environment — re-register on the device (Step 2).

7. Validate platform credentials — Android (FCM)

Android uses FCM v1 (the OAuth2 service-account path), not the deprecated legacy server-key API. Confirm:

  • The Firebase service-account JSON is uploaded and current — loaded by MAM_AndroidCredentials, with the JWT signed by MAM_FirebaseJwtSigner.
  • The service account can send FCM messages. MAM mints its access token with the firebase.messaging OAuth scope (https://www.googleapis.com/auth/firebase.messaging); if the Firebase Cloud Messaging API is disabled for the project or the service account lacks send permission, FCM rejects the request.
  • The FCM project id matches the Firebase project the app ships against.

If you’ve regenerated or rotated the service-account key in Firebase, the old JSON stops working — re-upload the new file.

8. VoIP / incoming-call pushes (iOS only)

VoIP pushes (incoming-call wakeups) are a separate path and a separate token. If normal notifications work but calls don’t ring:

  • VoIP uses the mam_ios_voip_token meta — a separate PushKit token from the standard mam_app_ios_pn_token. A standard APNs token will not receive VoIP pushes, and a VoIP token won’t receive standard ones — don’t expect either one to cover both jobs. (The server signs both with the same .p8 provider key; only the token and the APNs push-type differ.)
  • VoIP pushes are admin-triggered through the mam_send_voip_push_notification endpoint and dispatched with type = voip, which routes to MAM_IosVoipPushSender.
  • iOS enforces a strict deadline: the app must report the call to CallKit within roughly 5 seconds of receiving the VoIP push, or iOS kills the process and may ban further VoIP pushes for that app. If VoIP worked and then stopped, this ban is a likely cause — it’s an app-side timing issue, not a server credential problem.

Quick reference: symptom → likely cause

Symptom Most likely cause Step
Nothing arrives for any user, any type Credentials wrong/expired (APNs or FCM) 6, 7
Email works, push doesn’t, same notification Push channel toggled off for that type 4
One user gets nothing No / stale device token, or opted out 2, 3
Test send works, broadcast doesn’t Cron queue not draining 5
Worked yesterday, dead today (one device) Token churned (reinstall/restore) → BadDeviceToken 2
iOS only, sandbox/prod APNs environment mismatch 6
Calls don’t ring, notifications fine VoIP token/cert or CallKit 5s ban 8

  • Plugin: push-notification-manager — architecture of the dispatcher and platform senders.
  • Notifications Manager — configuring per-notification email / SMS / push channels and templates.
  • Cron in MAM Suite — how the cron list works and how to ensure jobs run on low-traffic sites.

What’s next

Once delivery’s restored, it’s worth deciding whether you want broadcasts to keep using the cron queue — smoother for large user bases, but only as reliable as your cron health — or to dispatch immediately, which is simpler but puts more load on a single request. That’s the mam_notification_use_cron setting from Step 5. If you do turn cron off, make sure your broadcasts stay within the load your server can handle in one request.

Was this article helpful?
Contents

    Need Support?

    Can't find the answer you're looking for? Don't worry we're here to help!