APNs push notification tester
Fill in the payload, pick the push type, drop your .p8 key with its Key ID and Team ID, and get a curl --http2 command that sends the push to api.sandbox.push.apple.com or api.push.apple.com from Terminal. Every field is checked against Apple's rules first: the 4096-byte limit (5120 for VoIP), the topic suffix and priority each push type needs, content-available for background pushes and the Live Activity keys. Signing happens in your browser: the key is not uploaded or stored.
Your key stays on this page
- •The .p8 key is read and used for signing by your own browser. It is never uploaded, and this page makes no network requests while you use it.
- •Nothing is saved: no cookies, no local storage. Reloading the page forgets the key, the device token and the payload.
- •A browser cannot send to APNs itself, so the page gives you a curl command to run in Terminal. The provider token in it works for up to an hour; keep it to yourself.
Header names, push types, topic suffixes, priorities, payload keys and size limits are copied from Apple's APNs documentation, checked on 2026-10-30.
1. Where it goes
The bytes from didRegisterForRemoteNotificationsWithDeviceToken (or a Live Activity push token), written as hexadecimal. Spaces and < > are removed for you.
Shows an alert, plays a sound or sets the badge.
Your bundle ID, plus the suffix the push type needs. Changing the push type swaps the suffix for you.
0 means try once and do not store. A future time means keep trying until then. Leave blank for Apple's normal storage.
Notifications with the same value replace each other on the device. At most 64 bytes. 0 of 64 bytes
A lowercase UUID that APNs repeats in its answer. Leave blank and APNs makes one.
2. How you sign in to APNs
Drop your AuthKey_XXXXXXXXXX.p8 file here
Or open the file, or paste its text below. The Key ID is filled in from the file name for you.
10 characters, shown next to the key under Certificates, Identifiers & Profiles > Keys.
10 characters, shown under Membership details in your developer account.
Apple: a provider token is valid for up to one hour. Make a new one no more than once every 20 minutes and no less than once every 60 minutes; changing it more often on one connection gives TooManyProviderTokenUpdates.
3. The payload
Alert
Badge and sound
Grouping and delivery
Live Activity
Your own keys
Apple ignores your own keys inside aps, so they go at the top level, for example {"gameID": "12345678"}.
4. Check and send
Payload size
65 of 4096 bytes
Counted as UTF-8 bytes of the minified JSON, the form curl sends. Chinese characters take 3 bytes, most emoji 4.
Fix these
- •Enter apns-topic (your bundle ID, plus a suffix for some push types).
- •Enter the device token. Until then the command reads it from $DEVICE_TOKEN.
- •Enter the Key ID.
- •Enter your Team ID.
curl command for Terminal
curl -i --http2 \
--header 'apns-push-type: alert' \
--header 'apns-priority: 10' \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data '{"aps":{"alert":{"title":"Hello","body":"This is a test push."}}}' \
"https://api.sandbox.push.apple.com:443/3/device/$DEVICE_TOKEN"Not signed yet: the command reads the token from $AUTHENTICATION_TOKEN.
macOS's own curl supports HTTP/2. A 200 answer with an apns-id header means APNs accepted the push; delivery to the device is not guaranteed.
Payload (pretty)
{
"aps": {
"alert": {
"title": "Hello",
"body": "This is a test push."
}
}
}Payload (minified, as sent)
{"aps":{"alert":{"title":"Hello","body":"This is a test push."}}}If APNs answers with an error
The answer body is JSON such as {"reason": "BadDeviceToken"}. Apple's list of reasons:
- 400 BadCollapseId
- apns-collapse-id is longer than 64 bytes.
- 400 BadDeviceToken
- The device token is not valid, or it belongs to the other server (sandbox vs production).
- 400 BadExpirationDate
- apns-expiration is not a valid value.
- 400 BadMessageId
- apns-id is not a valid UUID.
- 400 BadPriority
- apns-priority is not a valid value.
- 400 BadTopic
- apns-topic is not valid.
- 400 DeviceTokenNotForTopic
- The device token belongs to a different app or topic.
- 400 DuplicateHeaders
- A header appears more than once.
- 400 IdleTimeout
- The connection was idle too long.
- 400 InvalidPushType
- apns-push-type is not a valid value.
- 400 MissingDeviceToken
- The path has no device token.
- 400 MissingTopic
- apns-topic is missing and the certificate covers more than one topic.
- 400 PayloadEmpty
- The body is empty.
- 400 TopicDisallowed
- You may not push to this topic.
- 403 BadCertificate
- The certificate is not valid.
- 403 BadCertificateEnvironment
- The certificate is for the other server (sandbox vs production).
- 403 ExpiredProviderToken
- The provider token is stale (iat more than an hour old). Sign a new one.
- 403 Forbidden
- The action is not allowed.
- 403 InvalidProviderToken
- The provider token is not valid or its signature cannot be verified: wrong Key ID, Team ID or key.
- 403 MissingProviderToken
- No certificate was used and the authorization header is missing.
- 403 UnrelatedKeyIdInToken
- The token's key is not related to the key used for the first push on this connection. Open a new connection.
- 403 BadEnvironmentKeyIdInToken
- The key is limited to the other environment (sandbox vs production).
- 404 BadPath
- The :path is not valid.
- 405 MethodNotAllowed
- Only POST is allowed.
- 410 ExpiredToken
- The device token has expired.
- 410 Unregistered
- The device token is no longer active for this topic, usually because the app was deleted. Stop sending to it.
- 413 PayloadTooLarge
- The payload is over 4096 bytes (5120 for VoIP).
- 429 TooManyProviderTokenUpdates
- The provider token changed too often. Change it at most once every 20 minutes.
- 429 TooManyRequests
- Too many pushes to the same device token in a row. Wait and retry.
- 500 InternalServerError
- An error inside APNs. Retry later.
- 503 ServiceUnavailable
- APNs is unavailable. Retry later.
- 503 Shutdown
- This APNs server is shutting down. Reconnect.
How it works
- •The payload builder covers Apple's aps keys: alert (text or localized keys), badge, sound (including the critical-alert dictionary), thread-id, category, content-available, mutable-content, interruption-level, relevance-score, target-content-id, filter-criteria and the Live Activity keys (event, timestamp, content-state, stale-date, dismissal-date, attributes-type, attributes). Your own keys go beside aps. You can also paste a finished payload.
- •The size shown is the UTF-8 byte count of the minified JSON, which is what APNs measures. A Chinese character is 3 bytes and most emoji are 4, so a message can look short and still go over.
- •Headers follow Apple's list: apns-push-type, apns-topic with the right suffix (.voip, .complication, .push-type.liveactivity, .push-type.widgets, .location-query, .voip-ptt and the others), apns-priority with the per-type rules (background must be 5), apns-expiration, apns-collapse-id (64 bytes at most) and apns-id.
- •For token sign-in, the page signs a JWT with header {alg: ES256, kid} and claims {iss: Team ID, iat} using Web Crypto and your .p8 key, the same signer as the App Store Connect token page. Apple accepts a token for up to an hour and wants a new one no more than once every 20 minutes.
- •For certificate sign-in, the command uses --cert and --key with the files you name. A browser cannot send the request itself (APNs needs HTTP/2 from a server, and blocks web pages), so you run the command in Terminal.
What it does not do
- •It does not send the push. You run the curl command yourself; the page makes no network requests.
- •It does not keep keys, tokens or payloads. Reloading the page clears everything.
- •It cannot tell whether a device token is real, belongs to your app or matches the server you chose. APNs answers that (BadDeviceToken, DeviceTokenNotForTopic).
- •It checks that content-state and attributes are JSON, not that they match your Swift ActivityAttributes type.
- •It does not cover broadcast pushes to Live Activity channels or the channel management API.
Questions
- What do BadDeviceToken, TopicDisallowed and InvalidProviderToken mean?
- BadDeviceToken (400): the device token is not valid, most often because a development token was sent to production or the other way round. TopicDisallowed (400): you are not allowed to push to that apns-topic, for example a bundle ID outside your team. InvalidProviderToken (403): the JWT is not valid or its signature cannot be verified, usually a wrong Key ID or Team ID, or a key that does not match the kid. The full table of Apple's reasons is on this page under the curl command.
- What is the difference between the sandbox and production APNs servers?
- api.sandbox.push.apple.com is for apps installed from Xcode with the development aps-environment; api.push.apple.com is for TestFlight, Ad Hoc and App Store builds. Each build gets device tokens for its own server only. A key can work for both or be limited to one, depending on how you created it.
- How big can a push notification payload be?
- 4096 bytes for every push type except VoIP, which allows 5120 bytes. The limit counts the bytes of the JSON body as sent, in UTF-8, so non-Latin text and emoji use it up faster than the character count suggests. Over the limit, APNs answers 413 PayloadTooLarge.
- How often should I make a new provider token?
- Apple wants a fresh token at least once an hour, because APNs rejects a token whose iat is more than an hour old (ExpiredProviderToken), and no more than once every 20 minutes, because changing it too often on one connection gives TooManyProviderTokenUpdates. Reuse one token for all pushes in between.
- Why does my background push not arrive?
- Check the three things Apple requires: apns-push-type background, apns-priority 5 (10 is an error, and leaving the header out means 10), and an aps dictionary with only "content-available": 1, no alert, badge or sound. Even then the system treats background pushes as low priority and may hold them; Apple suggests no more than two or three an hour.
- Which apns-topic do I use?
- Your bundle ID for alert and background pushes. Other push types add a suffix: .voip for VoIP, .complication for watch complications, .push-type.liveactivity for Live Activities, .push-type.widgets for widgets, .push-type.controls for controls, .location-query for location pushes, .voip-ptt for Push to Talk and .pushkit.fileprovider for File Provider. The page swaps the suffix when you change the push type.