Push troubleshooting

Diagnose common iOS push notification issues and understand APNS error codes.

This guide helps you identify and resolve push notification problems, from environment mismatches and missing delivery receipts to APNS error codes. Each issue includes the likely cause and the action required.

On this page

Sandbox vs production

APNS has two environments. Mixing them up is the most common cause of "it works in debug but not TestFlight" (or vice versa).

The OS decides the environment based on how the app was built — you do not choose it in code. What you control is appEnvironment in ZTConfig:

Build typeappEnvironment value
Debug from Xcode.SANDBOX
TestFlight / App Store / Ad Hoc.PRODUCTION

A common pattern is to use a build configuration flag:

#if DEBUG
let environment: ZTAppEnvironment = .SANDBOX
#else
let environment: ZTAppEnvironment = .PRODUCTION
#endif
#ifdef DEBUG
ZTAppEnvironment environment = ZTAppEnvironmentSANDBOX;
#else
ZTAppEnvironment environment = ZTAppEnvironmentPRODUCTION;
#endif

Common mistakes

  • "Works in debug, not in TestFlight" — your backend is hitting the sandbox endpoint with a production token. TestFlight builds receive production tokens.
  • "Works in TestFlight, not from Xcode" — opposite problem. Backend is using the production endpoint for a sandbox token.
  • BadDeviceToken errors — the token and the APNS endpoint do not match. A sandbox token sent to the production endpoint (or vice versa) is rejected.

If you use an APNS Auth Key (.p8), a single key works for both environments — the backend picks the correct endpoint.

Nothing arrives on device

Work through this checklist in order:

  1. Device token — confirm the token is forwarded to the SDK in application(_:didRegisterForRemoteNotificationsWithDeviceToken:) via updateDeviceToken(token:). Print the token and verify your backend has it.
  2. Push Notifications capability — the main app's App ID must have Push Notifications enabled in the Apple Developer portal.
  3. Provisioning profiles — both the main app and the Notification Service Extension must be included in the provisioning profile (Development for debug, Distribution for release).
  4. APNS environment — confirm appEnvironment in ZTConfig matches the build type. See Sandbox vs production.
  5. Rate limiting — if you send too many notifications to the same device in a short period, APNS may throttle or drop them.

Notification Service Extension does not run

If notifications arrive but rich media is missing and delivery tracking does not fire:

  1. Test on a real device — Notification Service Extensions are not reliably exercised on the simulator.
  2. Check the payload — ZetaKit notifications must include com.zmp.ios.data in userInfo for canHandle(_:) to return true. If the payload is missing this key, the SDK does not process the notification.
  3. Check the target dependency — confirm ZetaNotificationService is linked to the extension target, not just the main app target.
  4. Check the extension's bundle ID — the extension needs its own App ID with App Groups enabled in the Apple Developer portal.

Tip: Add a temporary NSLog call to your extension's didReceive and check the device console in Xcode (filter by the extension's bundle ID). If the log does not appear, the extension is not being invoked.

For full setup instructions, see Push Notifications / NSE.

Rich media does not appear

If notifications arrive but images or videos are missing:

  1. Check the media URL — open it in a browser. It must be reachable over HTTPS and return a valid image content type.
  2. Check network access — the extension needs outbound network access. VPNs, firewalls, or App Transport Security rules can block the download.
  3. Check media size — the extension gets roughly 30 seconds total. Large files on a slow connection may not download in time. The notification is still delivered without the attachment.

Delivery tracking is not working

If notifications arrive and clicks register, but delivered events are missing in ZMP:

  1. App Group ID mismatch — the group ID must be identical in three places:
    • The main app's App Groups entitlement.
    • The extension's App Groups entitlement.
    • The string you pass to trackNotificationDelivered(_:appGroupId:).
  2. Call order — call trackNotificationDelivered before handleNotification. The delivery tracking data must be persisted before the content handler completes. Reversing the call order risks losing the delivery event.
  3. ZTConfig.appGroupId — the value you pass to ZTConfig on the main app side must match the extension. ZetaCore reads from that suite to pick up delivery records.
  4. Silent failure — an invalid or nil App Group ID fails silently by design. Double-check for typos.

Three panels side-by-side: Xcode app target entitlements showing the App Group, ZTConfig appGroupId parameter, and the ZetaNotificationService target entitlements — all showing the same App Group identifier

Build errors

ErrorFix
No such module 'ZetaNotificationService'Confirm the library is added to the extension target in your SPM dependencies (or the XCFramework is linked). Clean the build folder (Cmd+Shift+K) and rebuild.
Duplicate symbolsZetaNotificationService is linked to both the main app and the extension. Remove it from the main app target.
Minimum deployment targetSet the extension's deployment target to iOS 13.0 or higher.
Signing errorsThe extension needs its own App ID in the Apple Developer portal, included in both Development and Distribution provisioning profiles.

APNS error codes

These errors are returned by APNS when a push send fails. Some are caused by app-side configuration (marked Developer action); others originate at the provider layer (marked Escalate to Zeta).

Source: Apple: Handling error responses from APNs

Token errors

BadDeviceToken (HTTP 400) — Developer action

The device token is invalid. Common causes:

  • The token was not forwarded correctly via updateDeviceToken(token:).
  • The token belongs to one APNS environment (sandbox or production) but the send targeted the other. See Sandbox vs production.
  • The user uninstalled and reinstalled the app, generating a new token that has not yet been forwarded to the SDK.

Unregistered (HTTP 410) — Developer action

The device token is no longer active. This typically means:

  • The user uninstalled the app.
  • Push credentials (certificates or .p8 key) were rotated, and users registered with the previous credentials have stale tokens until the app re-registers them.

This error does not mean the user is push-disabled — only that a specific token was removed. It is common for testers who frequently install and uninstall the app.

Authentication errors

These indicate a mismatch between the push credentials and the app. They are configured on Zeta's side and require escalation.

ErrorHTTPDescription
InvalidProviderToken403The .p8 authentication key or .p12 certificate does not match the app's bundle ID or Team ID.
ExpiredProviderToken403The JWT authentication token has expired.
MissingProviderToken403No certificate or JWT token was provided with the request.
BadCertificate403The TLS client certificate is invalid.
BadCertificateEnvironment403The certificate is for the wrong APNS environment (sandbox vs production).

Resolution: Contact your Zeta account team. These errors originate in the push dispatcher's credential configuration.

Payload errors

PayloadTooLarge (HTTP 413) — Developer action

The notification payload exceeds the APNS limit (4 KB for regular notifications). If you use Additional Values in the ZMP campaign builder, large or numerous key-value pairs can push the payload over the limit. Reduce the number or size of Additional Values.

PayloadEmpty (HTTP 400) — Escalate to Zeta

The push request was missing a notification payload. This typically indicates a ZMP campaign misconfiguration.

BadRequestPayload (HTTP 400) — Escalate to Zeta

The JSON payload was malformed. Contact your Zeta account team.

Topic errors

TopicDisallowed (HTTP 400) — Escalate to Zeta

The bundle ID in the push request is not allowed for the authentication credentials being used. This can occur when a push type suffix is appended to the bundle ID. Contact your Zeta account team.

Rate limiting

TooManyRequests (HTTP 429) — Informational

Too many requests were sent to the same device token in a short period. APNS throttles delivery. The condition is temporary and resolves on its own.

TooManyProviderTokenUpdates (HTTP 429) — Escalate to Zeta

The provider authentication token is being refreshed too frequently (APNS allows updates no more than once every 20 minutes). This affects the push dispatcher and requires escalation to the Zeta engineering team.

Server errors

ErrorHTTPDescription
InternalServerError500An unexpected error on Apple's side. Transient — retry the send.
Shutdown / ServiceUnavailable503APNS is shutting down or temporarily unavailable. Transient — retry the send.

See also