Your iOS push fails with 400 and "reason": "BadDeviceToken". Before you delete that token, check one thing: is it going to the right APNs environment? Most of the time it's a sandbox token sent to production, or the other way round.
What the error looks like
APNs replies with HTTP 400 and a JSON body with the reason:
HTTP/2 400
apns-id: 5E3F9C1A-7B2D-4C8E-9A61-2F0B3D4E5A6C
{
"reason": "BadDeviceToken"
}Apple's description is simply that the device token is invalid. The response doesn't tell you why, so check the likely causes in order.
Cause 1: sandbox and production are mixed up
APNs has two environments, and a device token only works in the one it came from:
# Development builds (run from Xcode with a development profile)
https://api.sandbox.push.apple.com/3/device/<device-token>
# TestFlight, Ad Hoc and App Store builds
https://api.push.apple.com/3/device/<device-token>- A build run from Xcode with a development provisioning profile has
aps-environmentset todevelopmentand gets sandbox tokens. So does the iOS Simulator. - TestFlight, Ad Hoc and App Store builds are signed for distribution and get production tokens.
A typical story: push works perfectly while you test from Xcode, then every TestFlight tester gets BadDeviceToken, because the server still sends to the sandbox host. Or the reverse, after someone switches the server to production and keeps testing from Xcode.
Fix: store the environment with each token when the app uploads it (for example a sandbox flag set from your build configuration), and send each token to its matching host.
Other causes
- The token string was changed. APNs device tokens arrive as binary data. Convert them to a hexadecimal string once and store it exactly. Using
descriptionon the token, adding spaces or angle brackets, or cutting the string short all produce an invalid token. - You sent an FCM token to APNs. If your app uses Firebase, the token from
Messaging.messaging().fcmTokenis an FCM token. It goes to FCM, not to APNs directly. The APNs token is the one fromdidRegisterForRemoteNotificationsWithDeviceToken. - The token belongs to another app. Tokens are tied to the app that requested them. If you have several apps or bundle IDs, check that you send each token with the right one. This often shows up as
DeviceTokenNotForTopicinstead.
A quick way to find the cause
- Get a fresh token from a test device and log it on the device.
- Compare it with what your server stored, character by character.
- Note how that build was installed: from Xcode, or through TestFlight or the App Store.
- Send to the matching host. If it works, the problem was the environment.
- If it still fails on the right host with an exact token, check the bundle ID and your APNs key.
How it differs from other APNs errors
Only delete a token when the error says the token itself is dead:
| Reason | HTTP | What it means | What to do |
|---|---|---|---|
BadDeviceToken | 400 | The token is invalid for this request, often the wrong environment | Check the environment and token string first, then delete |
Unregistered | 410 | The token is no longer active for this topic | Delete the token |
DeviceTokenNotForTopic | 400 | The token doesn't belong to the apns-topic you sent | Set apns-topic to the app's bundle ID |
ExpiredProviderToken, InvalidProviderToken | 403 | Your signing JWT is too old or can't be verified | Fix the key ID, team ID or signing. Don't delete device tokens |
TooManyProviderTokenUpdates | 429 | You create a new JWT too often | Reuse one JWT. Apple asks you to refresh it no more than every 20 minutes and at least every 60 |
TooManyRequests | 429 | Too many pushes to the same device in a short time | Slow down and retry later |
InternalServerError, ServiceUnavailable | 500, 503 | A temporary problem at APNs | Retry with backoff |
In code, the branching can look like this:
// reason comes from the JSON body of a non-200 APNs response
switch (reason) {
case "Unregistered": // 410: the token is no longer active
await removeToken(token);
break;
case "BadDeviceToken": // 400: check the environment first
if (await isEnvironmentMismatch(token)) {
await sendToOtherHost(token);
} else {
await removeToken(token);
}
break;
case "DeviceTokenNotForTopic": // 400: wrong apns-topic (bundle ID)
await fixTopicConfig();
break;
case "TooManyRequests":
case "InternalServerError":
case "ServiceUnavailable":
await retryWithBackoff(token);
break;
}If you send iOS pushes through Firebase instead, APNs problems come back as FCM errors. See FCM UNREGISTERED: Requested entity was not found for that side.
On notifySphere
notifySphere delivers iOS pushes through FCM and APNs, and its React Native and Flutter SDKs register the device for you, so you don't convert or store tokens yourself. Still setting up iOS? Start with the Firebase iOS push guide, or fix No APNS token specified if you hit that first.
Frequently asked questions
What does APNs BadDeviceToken mean?
APNs returns 400 with the reason BadDeviceToken when the device token in the request path is invalid. The most common cause is sending a token to the wrong APNs environment: a sandbox token sent to api.push.apple.com, or a production token sent to api.sandbox.push.apple.com.
Should I delete a token that returns BadDeviceToken?
Only after you've ruled out an environment mismatch. If a development build's token is sent to the production host, the token is fine; it's going to the wrong place. Delete it if it also fails on the right host, or if the stored string was damaged.
What is the difference between BadDeviceToken and Unregistered?
BadDeviceToken (400) means the token isn't valid for that request, often because of the environment. Unregistered (410) means the token is no longer active for your app's topic, usually after an uninstall. A 410 response also includes a timestamp of when APNs last confirmed the token was invalid.
Why does BadDeviceToken happen only with TestFlight, or only in development?
TestFlight, Ad Hoc and App Store builds get production tokens. Builds run from Xcode with a development provisioning profile get sandbox tokens. If your server always uses one host, one of the two kinds of build will fail.
Do I need separate APNs keys for sandbox and production?
Not usually. A token-based (.p8) APNs key can work with both environments, though Apple lets you limit a key to one. If the key is limited to the wrong environment, APNs returns 403 BadEnvironmentKeyInToken, not BadDeviceToken. Certificate-based auth uses separate certificates, and the wrong one returns 403 BadCertificateEnvironment.