6 min read
Fix "APNS token has not been set yet" in Flutter firebase_messaging
Flutter firebase_messaging on iOS throws "APNS token has not been set yet"? Why getToken() fails, when the token never comes, and what changed in 17.
By Yevhen Beshkarov
- troubleshooting
- ios
- firebase
On iOS, Firebase delivers push notifications through Apple's push service (APNs), so FirebaseMessaging.instance.getToken() refuses to run until Apple has given the device its APNs token. Called earlier, it throws apns-token-not-set, which older versions of firebase_messaging describe as "APNS token has not been set yet". Usually the app just asked too early. How it should wait depends on the version: firebase_messaging 17 tells the app when Firebase is ready, and 16.7 doesn't. In a few setups, though, Apple never answers, however long the app waits.
I checked everything below on an iPhone 17 simulator with iOS 26.5, Xcode 27.2 and Flutter 3.44.2, with firebase_messaging 16.7.1 (Firebase iOS SDK 12.19.3) and 17.0.1 (Firebase iOS SDK 13.1.1).
APNS token has not been set yet, or has not been received on the device yet
The error comes in two wordings. Older versions say:
[firebase_messaging/apns-token-not-set] APNS token has not been set yet. Please ensure the APNS token is available by calling `getAPNSToken()`.Since October 2025 (firebase_messaging 16.0.3, with firebase_messaging_platform_interface 4.7.3), the same error says:
[firebase_messaging/apns-token-not-set] APNS token has not been received on the device yet. Please ensure the APNS token is available before calling `getAPNSToken()`.Both are a FirebaseException with the code apns-token-not-set, so check the code rather than the text. The hint to call getAPNSToken() is misleading, because that call never throws. It returns null until Apple has answered, which makes it the thing to wait on.
The check runs in Dart, before anything reaches iOS. getToken(), deleteToken(), subscribeToTopic() and unsubscribeFromTopic() all start with it. So a topic subscription at startup fails with the same error, while requestPermission() and onMessage work. If you use Firebase from Swift, the native SDK reports the same situation as No APNS token specified before fetching FCM Token.
Why getToken() fails right after the app starts
At launch, the plugin asks iOS to register the app for remote notifications, and iOS answers later, in application:didRegisterForRemoteNotificationsWithDeviceToken:. The Firebase iOS SDK keeps that answer in memory only. So an early getToken() fails on every launch, not just the first one, even when Firebase gave the app its FCM token on the previous launch.
How long the answer takes varies: about half a second on a simulator that had been running for a while, and about 30 seconds right after the simulator booted. On a phone, it depends on the network.
I checked this with a small app that calls getToken() right after Firebase.initializeApp(), after a fresh install and after a restart, with both versions. The call failed every time, although Apple answered within a second. What differs between the versions is whether the app learns that Firebase is ready.
firebase_messaging 16: wait for getAPNSToken()
With 16.7.1, the Firebase iOS SDK requests the FCM token by itself as soon as Apple answers (the log shows the app connecting to fcmtoken.googleapis.com), but the plugin doesn't pass it on to Dart: onTokenRefresh stayed silent for the 20 seconds I waited, after a fresh install and after a restart. So the app has to wait for getAPNSToken() itself, then call getToken():
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
/// The FCM token of this device once Apple has answered, or `null` when
/// Apple hasn't within [limit].
Future<String?> pushTokenWhenReady({
Duration limit = const Duration(minutes: 2),
}) async {
final messaging = FirebaseMessaging.instance;
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) {
var wait = const Duration(milliseconds: 500);
var waited = Duration.zero;
while (await messaging.getAPNSToken() == null) {
if (waited >= limit) return null;
await Future<void>.delayed(wait);
waited += wait;
if (wait < const Duration(seconds: 8)) wait *= 2;
}
}
return messaging.getToken();
}Don't await it before runApp(). I found apps on GitHub that opened to a white screen on iOS for exactly this reason: main() waited, getToken() threw, and runApp() never ran. Start the app first and send the result to your server when it arrives:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const MyApp());
final token = await pushTokenWhenReady();
if (token != null) await sendTokenToServer(token);
}If the wait runs out, try again when the user comes back to the app, for example from an AppLifecycleListener with onResume.
firebase_messaging 17: listen to onTokenRefresh
Since 17.0.0, released in October 2026, the plugin becomes the delegate of the Firebase iOS SDK during Firebase.initializeApp(), so onTokenRefresh gets two events that 16 lost. After a fresh install, it fires as soon as the SDK has the FCM token, in my runs 1.4 seconds after Apple answered. On every later launch, it fires right at startup with the value that the SDK saved last time: 0.2 seconds after main() started, before Apple had answered and while getToken() still threw.
So on 17, subscribe to onTokenRefresh right after initializeApp(), and keep the getToken() call for Android, where it doesn't depend on Apple:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
final messaging = FirebaseMessaging.instance;
messaging.onTokenRefresh.listen(sendTokenToServer);
runApp(const MyApp());
try {
final token = await messaging.getToken();
if (token != null) await sendTokenToServer(token);
} on FirebaseException catch (e) {
// On iOS before Apple has answered: onTokenRefresh delivers it.
if (e.code != 'apns-token-not-set') rethrow;
}
}Your server may get the same value twice this way, so it should accept one that it already has. Subscribe early, because onTokenRefresh is a broadcast stream and a listener that comes after the startup event never sees it. If your code subscribes later, after a sign-in for example, wait for getAPNSToken() as with 16.
Firebase is also moving Cloud Messaging from registration tokens to the Firebase Installation ID (FID). Version 17 deprecates getToken(), onTokenRefresh and deleteToken() in favor of register(), onRegistered and unregister(), and flutter analyze exits with code 1 on the code above because of the deprecation infos. I haven't tried the FID path yet, so this article stays with getToken().
When the APNs token never comes
If getAPNSToken() still returns null after a minute, waiting longer won't help. These are the cases I know of.
No aps-environment entitlement
iOS registers an app for remote notifications only when the build is signed with the aps-environment entitlement, which Xcode adds with the Push Notifications capability in Signing & Capabilities. Without it, the Xcode console shows:
[FirebaseMessaging][I-FCM012002] Error in application:didFailToRegisterForRemoteNotificationsWithError: no valid "aps-environment" entitlement string foundThe simulator follows the same rule. A build made with flutter build ios --simulator --no-codesign doesn't get the entitlement, and mine waited two minutes for Apple in vain.
A free Apple team
A personal team can't sign aps-environment. To run the app on an iPhone, you have to remove the capability, and then Apple never answers there. The simulator doesn't need a team, since Xcode signs simulator builds ad hoc.
The iOS simulator on GitHub Actions
The iOS simulator on a Mac gets a real APNs token. On a macOS virtual machine it never does, and the macOS runners of GitHub Actions are virtual machines. I tried a macos-26 runner: the app was built with the entitlement, nothing came in four minutes, and the push daemon logged no token info found in keychain before it disconnected. actions/runner-images#12747 reports the same for macos-15. A CI job on a hosted runner can check that the app starts without push notifications, but not that it gets them.
firebase_messaging 16.5.0 and 16.6.0
These two versions skipped the registration for remote notifications at launch in two common setups: when Firebase is initialized from Dart, as with Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform) (flutterfire#18643), and in apps with scenes (flutterfire#18555), which the iOS template of current Flutter creates with SceneDelegate.swift. getAPNSToken() then stayed null on every launch. Version 16.7.0 fixed both. Check which version your app has:
flutter pub deps | grep firebase_messagingAuto-init is off
With FirebaseMessagingAutoInitEnabled set to NO in Info.plist, or after setAutoInitEnabled(false), the plugin doesn't register for remote notifications. Call setAutoInitEnabled(true) before you start waiting.
Not the APNs key
The APNs key in the Firebase console doesn't belong on this list. Firebase needs it to send messages through Apple, so without it nothing arrives, but getAPNSToken() and getToken() work all the same.
How SMF handles push notifications on iOS
The firebase_messaging module of SMF, a free, open-source Flutter project generator, gives the app the address of the device as appPush.address, which stays null until Firebase has one. The first frame never waits for it. On iOS, the module polls getAPNSToken() as above, waiting from half a second up to eight seconds between tries, for at most two minutes, and asks again whenever the user comes back to the app. It adds aps-environment and the background mode remote-notification, and the README of the app explains what to do with a free team. Uploading the APNs key stays a step you do by hand. SMF's CI starts the apps on the iOS simulator of a hosted runner, where they have to start and work without an address.