Push notification environments in iOS: sandbox vs production, the .p8 key and Firebase setup

APNs has sandbox and production environments, and every link in the push chain must match: profile, entitlement, token, key and endpoint.

Read time
18 min
Word count
2.9K
Sections
13
FAQs
8
Share
Push flow graphic: an eCorpOS iPhone alert routed through Firebase Cloud Messaging to APNs sandbox and production servers
On this page · 13 sections
  1. What is an APNs environment?
  2. Sandbox vs production: the roles they play
  3. The aps-environment entitlement: why the profile decides
  4. Which build uses which environment
  5. Why notifications work in development but not in production
  6. Generating the .p8 APNs authentication key
  7. Uploading the key to Firebase
  8. APNs tokens vs Firebase identifiers
  9. What to check in Xcode
  10. Delivery is not the same as presentation
  11. Reading APNs error codes
  12. How eCorpIT can help
  13. References

Summary. Apple Push Notification service (APNs) has two environments, sandbox (development) and production, and every link in the push chain must match. Builds run from Xcode with a development profile get sandbox device tokens, while TestFlight, Ad Hoc and App Store builds get production tokens. Xcode derives the aps-environment entitlement from your provisioning profile, and a distribution profile allows only production. Send each token to its matching endpoint, and give Firebase an APNs key that covers the environment you're testing.

Many iOS developers hit this problem at some point. Push notifications arrive reliably while you test from Xcode, then stop as soon as the app reaches TestFlight or the App Store. Nothing in the app's code may have changed, so it looks like a code bug. Very often it is an environment mismatch somewhere in the delivery chain.

This guide explains what each environment does, why aps-environment isn't a value you simply type in, how to create the .p8 key, how to upload it to Firebase, how APNs tokens differ from Firebase identifiers, and what to check in Xcode before you ship.

About this article. This guide is based on Apple's documentation for APNs and the User Notifications framework, and on Firebase's Cloud Messaging guides for Apple platforms and Flutter, as checked on 30 September 2026. Portal and console screens change from time to time, so menu names may differ slightly in your account. Researched with AI assistance and reviewed by our editorial team.

What is an APNs environment?

When your app registers for remote notifications, APNs gives it a device token. Apple describes this token as effectively the address of your app on that device, and every app needs its own token, even on the same device. Your provider server, or Firebase acting for you, uses that address to send notifications through APNs.

APNs runs its two environments on two servers:

  • Development (sandbox) server: api.sandbox.push.apple.com
  • Production server: api.push.apple.com

Both accept HTTP/2 over TLS 1.2 or later on port 443, or port 2197 if you need to let APNs traffic through a firewall that blocks other HTTPS traffic. Apple's guidance is to use the production server for shipping apps and the development server for testing. Apple also says the development environment is the same thing as the sandbox environment, so the two names are interchangeable.

Sandbox vs production: the roles they play

Aspect Sandbox (development) Production
Purpose Testing while you build Real users on shipping builds
APNs server api.sandbox.push.apple.com api.push.apple.com
Device tokens Issued to development builds Issued to production builds
Typical builds Run from Xcode with a development profile TestFlight, Ad Hoc and App Store builds

A device token belongs to one environment. Apple's archived technical note on push troubleshooting says each push environment issues a different token for the same device, and that APNs treats a token sent to the wrong environment as invalid and discards the notification. Apple's current error reference says the same about BadDeviceToken: check that the token matches the environment.

So the environment isn't a cosmetic setting. It decides which endpoint can deliver to which build.

The aps-environment entitlement: why the profile decides

Your app's code signature includes an entitlement called aps-environment, which tells the system whether to register with the development or the production APNs environment. It is used by both the User Notifications and PushKit frameworks.

Apple's documentation explains why it rarely behaves like a setting you control. Xcode sets the value from your app's current provisioning profile: a development profile gives development, while production profiles and TestFlight builds use production. Apple notes that these defaults can be modified, but any value you set still has to be allowed by the provisioning profile.

An Apple Developer Technical Support engineer explained the mechanism on the developer forums. A provisioning profile carries an allowlist of entitlements, and for aps-environment a distribution profile's allowlist always contains production. That's why a developer who tried to force development onto a distribution build couldn't do it.

Two practical lessons follow:

  • Don't try to hard-code the value. Treat aps-environment as something your signing setup derives, and fix the signing inputs instead.
  • Add the capability rather than typing the key. Apple says you add this entitlement by enabling the Push Notifications capability in Xcode.

Which build uses which environment

Use this as a quick reference. What decides the environment is the provisioning profile the build is signed with, not the words Debug or Release.

How the app is installed Signed with APNs environment
Run from Xcode onto a device Development profile Sandbox (development)
Distributed through the App Store Distribution profile Production
TestFlight build Distribution profile Production
Ad Hoc distribution build Distribution profile Production

Apple's technical note confirms the split: development builds connect to the sandbox, while Ad Hoc and distribution builds connect to production. A Release build configuration run from Xcode with a development profile is still a sandbox build, because the profile hasn't changed. Likewise, a Debug configuration doesn't by itself mean sandbox. The profile is what counts.

Why notifications work in development but not in production

If pushes reach your Xcode builds and then stop on TestFlight or the App Store, check these four causes.

1. Your backend is still sending to the sandbox endpoint

Production builds produce production device tokens. If your server keeps sending them to the sandbox host, APNs rejects them, typically with BadDeviceToken. Apple's technical note suggests running a separate provider instance for each environment to avoid exactly this.

2. Your credentials can't authenticate production requests

APNs keys can be limited to Sandbox or Production, and APNs returns BadEnvironmentKeyIdInToken when the key ID in your token doesn't match the environment. In Firebase, if the only credentials you've configured can't authenticate production requests, production delivery can fail.

3. The app was never signed with the right entitlement

If aps-environment is missing from the final signature, the app can't register properly. Apple's technical note says the usual reasons are that push notifications weren't enabled for the App ID, or that the distribution provisioning profile wasn't regenerated after enabling them.

4. An old token is being used

Apple says your app should register and receive its device token each time it launches, and its technical note warns against storing a token and reusing it, because the token can change. A token captured from a development build won't work for a production install.

The underlying fix is the same in every case: make each part of the delivery chain consistent with the environment.

Generating the .p8 APNs authentication key

Apple supports two ways for a server to authenticate with APNs: certificates and tokens. Token-based authentication uses a .p8 signing key. Apple describes it as stateless, and notes that you can use the same token from several provider servers, and one token for all or some of your company's apps. Creating keys needs the Account Holder or Admin role.

Steps

  1. Sign in to your Apple Developer account and open Certificates, Identifiers & Profiles.
  1. Choose Keys, then click the add (+) button.
  1. Enter a key name and select Apple Push Notification service (APNs).
  1. Click Configure next to Apple Push Notification service, then choose the environment and the key type, Team Scoped or Topic Specific.
  1. Continue, register the key, and download the .p8 file.
  1. Note the Key ID, a 10-character string, and your 10-character Team ID. Both go into every authentication token you create.

Environment-specific keys and older keys

Apple now offers environment-specific keys. Team-scoped keys work for any topic in your team but are restricted to either Sandbox or Production, with a maximum of two keys per environment. Topic-specific keys cover chosen topics within a single environment, with up to 200 keys per environment and up to 400 topics per key.

Existing team-scoped keys that work in both environments are still supported, so an older key isn't invalid. Apple still recommends separate, environment-specific keys, because they create distinct workflows and make your development process safer to manage.

Things to know before you continue

  • Store the file safely. The .p8 file is the private key that signs your authentication tokens, and it must stay private. Apple doesn't keep a copy in your developer account, and you can't download it again, so save it somewhere secure straight away.
  • Replace a compromised key carefully. Create a new key first, move your servers to it, then revoke the old one. Apple also recommends closing your existing HTTP/2 connections to APNs and opening new ones.
  • Refresh tokens on schedule if you sign them yourself. If you send directly to APNs, refresh your token no more than once every 20 minutes and no less than once every 60 minutes. APNs rejects tokens more than an hour old with ExpiredProviderToken, and it returns TooManyProviderTokenUpdates if you refresh too often. Firebase handles this for you.

Uploading the key to Firebase

Firebase Cloud Messaging (FCM) sits between your backend and APNs. For Apple devices, FCM needs your APNs credentials so it can talk to Apple on your behalf.

Steps in the Firebase console

  1. In the Firebase console, go to Settings, then General.
  1. Click the Cloud Messaging tab.
  1. Under APNs authentication key in the iOS app configuration section, click Upload.
  1. Upload your development key, your production key, or both. Firebase requires at least one.
  1. Select the .p8 file and enter its Key ID. Firebase's Flutter guide also asks for your Apple team ID, so keep it ready.
  1. Click Upload, or Save, to finish.

Our recommendation

Firebase doesn't require both keys. But if your team tests both development and production builds, upload credentials that cover each environment. This keeps the configuration explicit and matches Apple's advice to use environment-specific keys. If you upload only a development key, confirm that your production builds can still be authenticated before you release.

APNs tokens vs Firebase identifiers

This is a common source of confusion. When you use Firebase, there are two different identifiers, and they aren't interchangeable:

  • The APNs device token is issued by Apple when your app registers with APNs.
  • The FCM registration token is issued by Firebase, and it is what you normally use to target a device from your backend or the Firebase console.

The flow looks like this:


            iOS app
   ↓  registers with APNs
APNs device token
   ↓  passed to Firebase Messaging
FCM registration token
   ↓  stored by your backend
Your server / Firebase console
   ↓
FCM  →  APNs  →  iPhone
          

Firebase's current Apple-platform guide also describes registering with a Firebase Installation ID (FID), which you can use to target notifications. You opt in with the FirebaseMessagingInstallationIdEnabled key in your app's Info.plist, and the SDK delivers the FID through a messaging delegate method. Check which identifier your integration and SDK version use, and never send an APNs token where Firebase expects one of its own identifiers.

Mapping the APNs token

By default, the FCM SDK maps the APNs token for you through method swizzling, which you can turn off with the FirebaseAppDelegateProxyEnabled flag. Firebase says that if you've disabled swizzling, or you're building a SwiftUI app, you must map the token yourself:


            func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    Messaging.messaging().apnsToken = deviceToken
}
          

SwiftUI apps provide this app delegate through the UIApplicationDelegateAdaptor property wrapper. Flutter is different: Firebase says the FCM Flutter plugin requires method swizzling on Apple devices, so don't disable it in a Flutter app.

Wait for the APNs token before making FCM calls

Firebase's Flutter guide warns that in iOS SDK 10.4.0 and later, the APNs token must be available before you make FCM API requests, and that it isn't guaranteed to have arrived yet. In Flutter, check getAPNSToken() before calling the FCM token APIs.

Handle token refresh

Don't tie a user's account permanently to one FCM token. Firebase says the registration token may change when the app is restored on a new device, when the user reinstalls the app, or when they clear app data. Store the current token and update your backend whenever it changes:

  • Native iOS: implement messaging(_:didReceiveRegistrationToken:), which Firebase says fires at each app start and whenever a new token is generated.
  • Flutter: subscribe to FirebaseMessaging.instance.onTokenRefresh, which Firebase describes the same way.

What to check in Xcode

Work through this list on the target that ships to users. If you're moving to the latest toolchain as well, our Xcode 27 and Swift 6.4 guide covers the wider changes.

1. Add the Push Notifications capability

Open the target, go to Signing & Capabilities, click + Capability, and add Push Notifications. This is what adds the aps-environment entitlement.

2. Confirm signing and team

Select the correct team. With Automatically manage signing, Xcode chooses the profile for each build. With manual signing, make sure the profile matches the build type: development for local runs, distribution for archives.

3. Match the bundle identifier everywhere

The bundle ID in Xcode must match the App ID in your developer account, the app registered in Firebase, and the GoogleService-Info.plist in your project. Apple says the push topic is generally your app's bundle ID, sometimes with a suffix for particular push types. A mismatch can produce errors such as DeviceTokenNotForTopic.

4. Check the App ID in the developer portal

Make sure Push Notifications is enabled for the App ID. If you enabled it after creating a distribution profile, regenerate the profile.

5. Add background modes only if you need them

To receive background (silent) notifications, Apple says you must add the Background Modes capability and select Remote notifications. Firebase's Flutter guide enables both Background fetch and Remote notifications as part of its setup. For a native app that sends only ordinary alerts, check whether you need them.

6. Verify the runtime code

Confirm that your code requests notification authorisation, calls registerForRemoteNotifications(), and, where required, sets Messaging.messaging().apnsToken.

7. Inspect the built app

Don't assume the entitlement is correct. Check what was actually signed into the build:


            # Entitlements embedded in the built app, as XML
codesign -d --entitlements - --xml /path/to/YourApp.app

# Provisioning profile embedded in the app
security cms -D -i /path/to/YourApp.app/embedded.mobileprovision
          

Look for aps-environment. It should read development for local runs and production for TestFlight and App Store builds.

8. Test on a real device, with the right build type

Test development pushes from an Xcode-installed build. Then test production pushes from a TestFlight build, which is signed for production. Don't judge production behaviour from a build installed straight from Xcode. Our iOS 27 API changes and testing checklist covers the rest of a release test pass.

9. Log both tokens

During development, log the APNs device token and the FCM registration token. Confirm that Firebase Messaging receives the APNs token, and that your backend stores the current FCM registration token rather than an old one.

Delivery is not the same as presentation

A correct environment setup doesn't guarantee that someone sees a banner. A push passes through several stages, and each can fail or behave differently:


            FCM accepts the message
   ↓
APNs accepts the message
   ↓
The device receives the push
   ↓
iOS decides how to handle it
   ↓
Banner / sound / badge / background handling
          

An APNs 200 response means APNs accepted the request. It doesn't mean the person saw the notification. Apple calls APNs a best-effort service that may reorder notifications sent to the same device token, with exact behaviour depending on how the person uses your app and on the device's power state. Depending on battery, connectivity and other conditions, APNs may deliver a notification immediately, defer it, try several times, or discard it. If it can't deliver several notifications for the same app while a device is offline, it keeps only one.

Check notification permission first

Even with a perfect production setup, someone who has turned notifications off will see nothing. Check the current status:


            UNUserNotificationCenter.current().getNotificationSettings { settings in
    print(settings.authorizationStatus)
}
          

Separate the problem into layers

When notifications go missing, work out which layer is failing before you change anything:

  • APNs registration or token problem: no valid device token is being produced or forwarded.
  • FCM token problem: the backend holds an old or wrong registration token.
  • Authorisation problem: the person hasn't allowed notifications.
  • Delivery problem: APNs rejected or throttled the request. Read the error code.
  • Presentation problem: the push arrived, but the app or system chose not to show it, for example because of how your UNUserNotificationCenterDelegate handles foreground notifications, the payload type, or the person's notification settings.

Silent notifications need extra care. Apple treats them as low priority, doesn't guarantee their delivery, and may throttle them, and it advises sending no more than two or three an hour. Those limits are disabled when you test from Xcode, so a silent push that works in testing can behave differently on real users' devices.

Reading APNs error codes

If you send to APNs yourself, or read delivery logs, these error strings point at environment and credential problems:

Error What Apple says it means Likely fix
BadDeviceToken The token is invalid, or doesn't match the environment Send sandbox tokens to sandbox and production tokens to production
BadEnvironmentKeyIdInToken The key ID in the provider token doesn't match the environment Use a key scoped to the environment you're sending to
BadCertificateEnvironment The client certificate doesn't match the environment Use the matching certificate, or move to a .p8 key
DeviceTokenNotForTopic The device token doesn't match the specified topic Check the bundle ID and topic
InvalidProviderToken The provider token isn't valid, or its signature can't be verified Check the Key ID, Team ID and .p8 file
ExpiredProviderToken The provider token is stale Generate a fresh provider token
Unregistered The device token is inactive for the topic Stop sending to it unless the app registers the same token again

Apple says not to retry requests that fail with BadDeviceToken, DeviceTokenNotForTopic, Forbidden, ExpiredToken, Unregistered or PayloadTooLarge. You can retry 5XX responses after 15 minutes, ideally with back-off, and TooManyRequests after a delay.

For testing, the apns-unique-id response header is available only in the development environment. Use it to look up a notification's delivery log in the Push Notifications Console, whose delivery logs also cover only the development environment.

How eCorpIT can help

eCorpIT is a Gurugram-based technology organisation, founded in 2021, assessed at CMMI Level 5 and MSME certified, with senior-led engineering teams working across AWS, Microsoft and Google platforms. Our iOS app development team in India audits push setups end to end: signing and aps-environment, APNs keys per environment, the Firebase configuration, and how your backend stores and refreshes tokens. For cross-platform apps, our Flutter app development team handles the FCM plugin's iOS requirements. If you need more capacity for a release, you can hire iOS developers from our team. Talk to us at /contact-us/.

Last updated: 30 September 2026.

References

  1. APS Environment Entitlement — Apple Developer Documentation
  1. Registering your app with APNs — Apple Developer Documentation
  1. Sending notification requests to APNs — Apple Developer Documentation
  1. Establishing a token-based connection to APNs — Apple Developer Documentation
  1. Handling notification responses from APNs — Apple Developer Documentation
  1. Troubleshooting push notifications — Apple Developer Documentation
  1. Pushing background updates to your app — Apple Developer Documentation
  1. Testing notifications using the Push Notification Console — Apple Developer Documentation
  1. Technical Note TN2265: Troubleshooting Push Notifications (archived) — Apple Developer
  1. Forcing the APNs environment to development on distribution builds (DTS Engineer reply) — Apple Developer Forums
  1. Create a private key to access a service — Apple Developer Account Help
  1. Communicate with APNs using authentication tokens — Apple Developer Account Help
  1. Get started with Firebase Cloud Messaging in Apple platform apps — Firebase
  1. Get started with Firebase Cloud Messaging in Flutter apps — Firebase
  1. Viewing the status of push notifications using Metrics and APNs — Apple Developer Documentation

Frequently asked

Quick answers.

01 What is the sandbox environment in APNs?
It is Apple's name for the development environment of APNs, served from api.sandbox.push.apple.com. Apple says development and sandbox mean the same thing. It is meant for testing builds run from Xcode with a development profile, while shipping apps use the production server, api.push.apple.com. Tokens from one environment don't work in the other.
02 Can I edit aps-environment myself?
Only within limits. Xcode sets the value from your provisioning profile, and Apple says the defaults can be modified, but the value must be allowed by that profile. An Apple Developer Technical Support engineer has explained that a distribution profile's allowlist always contains production, so you can't force development onto a distribution build.
03 Why do my pushes work from Xcode but not from TestFlight?
TestFlight builds use the production environment, while builds installed from Xcode with a development profile use the sandbox. If your backend still sends to the sandbox server, or your credentials only cover the sandbox, production tokens are rejected. The same applies to Release builds: the profile decides the environment, not the build configuration.
04 What is a .p8 file, and can I download it again?
It is the private key Apple gives you for token-based APNs authentication. You can't download it again: Apple doesn't keep a copy in your account, so store it securely. Apple recommends environment-specific keys, so many teams create one for Sandbox and one for Production. Older keys that cover both environments still work.
05 Do I have to upload both keys to Firebase?
No. Firebase requires at least one and lets you upload a development key, a production key, or both. If your team tests both development and production builds, uploading credentials for each environment keeps the setup explicit. If you upload only a development key, confirm that production builds can still authenticate before release.
06 Is the APNs device token the same as the FCM registration token?
No. Apple issues the APNs token when your app registers with APNs. Firebase issues the FCM registration token, which you normally use to target a device, and it maps the APNs token to its own identifier automatically through swizzling, or when you set the apnsToken property yourself. Never send one where the other is expected.
07 My APNs request returned 200, but nothing appeared. Why?
A 200 only means APNs accepted the request. APNs is best-effort and may defer or discard notifications depending on power and connectivity. Then check notification permission, whether the app was in the foreground and how your delegate presents notifications, the payload type, and the person's notification settings on the device.
08 How do I confirm which environment my build is using?
Inspect the signed app with codesign -d --entitlements - --xml and read the aps-environment value: development for local runs, production for TestFlight and App Store builds. You can also decode the embedded provisioning profile with security cms -D -i. Always check the actual build you're testing, not the project settings.

About the author

Aman Mathur

Software Engineer | Flutter

Software Engineer with 3+ years building high-performance, AI-powered cross-platform mobile apps for global clients.

Subscribe

One engineering note a week. No fluff, no spam.

Senior-architect playbooks on AI agents, mobile apps, cloud, security, data, and marketing — delivered every Wednesday.

Past the reading

Read enough. Let's build something.

A senior architect responds in 24 working hours with scope, indicative cost, and a timeline. NDA before any technical conversation.