APNs error codes, and what to do about each
Every APNs error reason Apple documents, from BadDeviceToken and DeviceTokenNotForTopic to 410 Unregistered and the provider token errors, with fixes.
When a push to api.push.apple.com fails, Apple answers with an HTTP status and a JSON body with one reason. The reason tells you what to fix. This page lists every reason Apple documents, grouped by status, with the usual cause, the fix, and whether to retry.
HTTP/2 410
apns-id: 6A7F2C10-...
{ "reason": "Unregistered", "timestamp": 1761638400000 }
A success is 200 with an empty body. An error body has reason, and on a 410 also timestamp: the time, in milliseconds, at which Apple confirmed the token was no longer valid.
The errors you will see most
| reason | Status | Usual cause | Fix |
|---|---|---|---|
BadDeviceToken |
400 | A sandbox token sent to production, or the other way round; or a token stored with spaces or brackets; or an FCM token sent to APNs | Send development builds to api.sandbox.push.apple.com and TestFlight and App Store builds to api.push.apple.com. Store the token as plain hex |
DeviceTokenNotForTopic |
400 | apns-topic is not the bundle id of the app that made the token |
Use the app’s exact bundle id, plus .voip, .complication or .push-type.liveactivity for those push types |
Unregistered |
410 | The app was deleted, or the token became inactive for the topic | Delete the token |
ExpiredToken |
410 | The device token has expired | Delete the token; the app registers a new one when it next runs |
InvalidProviderToken |
403 | The JWT’s Key ID (kid) or Team ID (iss) does not match the .p8 key, the key was revoked, or the signature is wrong |
Check the Key ID and Team ID in the Apple Developer account; sign with ES256 |
ExpiredProviderToken |
403 | The JWT is more than an hour old | Make a new token every 20 to 60 minutes |
TooManyProviderTokenUpdates |
429 | A new JWT was made for every request, or more often than every 20 minutes | Make one token and reuse it |
PayloadTooLarge |
413 | The payload is over 4 KB (5 KB for VoIP) | Send less: move data to your server and send an id |
TooManyRequests |
429 | Too many pushes in a row to the same device | Wait, then retry |
Every APNs error reason
From Apple’s table of response error strings:
| Status | reason | Meaning |
|---|---|---|
| 400 | BadCollapseId |
The apns-collapse-id is over the maximum size (64 bytes) |
| 400 | BadDeviceToken |
The device token is invalid or does not match the environment |
| 400 | BadExpirationDate |
The apns-expiration value is invalid |
| 400 | BadMessageId |
The apns-id value is invalid |
| 400 | BadPriority |
The apns-priority value is invalid |
| 400 | BadTopic |
The apns-topic value is invalid |
| 400 | DeviceTokenNotForTopic |
The device token does not match the topic |
| 400 | DuplicateHeaders |
A header is repeated |
| 400 | IdleTimeout |
The connection was idle too long |
| 400 | InvalidPushType |
The apns-push-type value is invalid |
| 400 | MissingDeviceToken |
No device token in the request path |
| 400 | MissingTopic |
No apns-topic header where one is required |
| 400 | PayloadEmpty |
The payload is empty |
| 400 | TopicDisallowed |
Pushing to this topic is not allowed |
| 403 | BadCertificate |
The certificate is invalid |
| 403 | BadCertificateEnvironment |
The certificate is for the other environment |
| 403 | ExpiredProviderToken |
The provider token is stale; make a new one |
| 403 | Forbidden |
The action is not allowed |
| 403 | InvalidProviderToken |
The provider token is not valid or its signature cannot be verified |
| 403 | MissingProviderToken |
No certificate on the connection and no provider token in authorization |
| 403 | UnrelatedKeyIdInToken |
The token’s key id is not related to the one used first on this connection; open a new connection |
| 403 | BadEnvironmentKeyIdInToken |
The token’s key id does not match the environment |
| 404 | BadPath |
The :path is invalid; it must be /3/device/<token> |
| 405 | MethodNotAllowed |
The method is not POST |
| 410 | ExpiredToken |
The device token has expired |
| 410 | Unregistered |
The device token is inactive for the topic |
| 413 | PayloadTooLarge |
The payload is too large |
| 429 | TooManyProviderTokenUpdates |
The provider token is updated too often; at most once every 20 minutes |
| 429 | TooManyRequests |
Too many requests in a row to the same device token |
| 500 | InternalServerError |
An internal error at Apple |
| 503 | ServiceUnavailable |
The service is unavailable |
| 503 | Shutdown |
The APNs server is shutting down |
Sandbox or production: the cause of most BadDeviceToken
A token belongs to one APNs environment:
| Build | Environment | Send to |
|---|---|---|
| Run from Xcode onto a phone | Development (sandbox) | api.sandbox.push.apple.com |
| TestFlight | Production | api.push.apple.com |
| App Store | Production | api.push.apple.com |
The aps-environment entitlement in the build decides it. A token key (.p8) works for both environments, so the same key is fine; the host is what must match. If you cannot tell which kind of token you have, try production and, on BadDeviceToken, try sandbox before deleting the token.
The provider token (JWT)
With a .p8 key, every request carries authorization: bearer <JWT>, a token you sign yourself:
- Header:
{ "alg": "ES256", "kid": "<Key ID>" } - Claims:
{ "iss": "<Team ID>", "iat": <seconds now> } - Signed with the
.p8private key, ES256.
Four errors come from getting this wrong. InvalidProviderToken: a wrong Key ID or Team ID, or a revoked key. ExpiredProviderToken: older than an hour. TooManyProviderTokenUpdates: refreshed more than once every 20 minutes. MissingProviderToken: no header at all. Make one token, keep it in memory, and replace it after about 50 minutes.
A retry rule that holds up
Apple’s own guidance, as a rule your sender can follow:
- 200: done.
BadDeviceToken,DeviceTokenNotForTopic,Unregistered,ExpiredToken: do not retry the token. Delete it if it is really dead; forBadDeviceTokencheck the environment first, and forDeviceTokenNotForTopiccheck your bundle id, since one wrong setting fails every token.Forbidden,PayloadTooLarge: do not retry as is.- Other 4xx: fix the header, token or payload the reason names, then send again.
TooManyRequests: retry after a delay.- 5xx: wait, then retry with backoff. Apple suggests waiting 15 minutes before retrying a 5xx payload.
Errors when iOS goes through FCM
If your iOS app uses Firebase Messaging, your server talks to FCM and FCM talks to APNs. You then see FCM’s codes instead: a bad or missing APNs key in the Firebase project comes back as THIRD_PARTY_AUTH_ERROR, and a dead iPhone token as UNREGISTERED. See FCM error codes.
What SignOps does with these
SignOps Push can send to iOS straight to APNs with your .p8 key. With the environment set to Production and sandbox, a token that gets BadDeviceToken from production is tried on sandbox before it counts as failed. A token Apple answers with 410, Unregistered, BadDeviceToken or DeviceTokenNotForTopic is removed and your webhook gets device.removed; 429 and 5xx answers are retried. One provider token is made per key and reused for 50 minutes. The dashboard’s See what Apple said shows the reason for each device. The setup, from making the .p8 key onward, is in the iOS push guide.
Sources: Apple: Handling notification responses from APNs, Apple: Sending notification requests to APNs, Apple: Establishing a token-based connection to APNs.