Notification Payload is a JSON structure that the server sends via APNS to an iOS device, defining the content of a push notification and the behavior upon receipt. The payload includes mandatory and optional keys that control text, sound, badge, media attachments, and background processing. According to Apple Developer Documentation, 2026, the maximum payload size is 4096 bytes for regular notifications and 5120 bytes for VoIP push, imposing strict limits on the amount of data transferred.
Key Takeaways
Notification Payload is a JSON object that the server sends to APNS (Apple Push Notification Service) for delivery to an iOS device. The payload contains all the data the system needs to display the notification: title, text, sound, badge, and metadata for background processing. The payload structure is strictly regulated by Apple and includes mandatory keys for proper system processing.
When the server sends a push notification through the APNS HTTP/2 API, the request contains authorization headers and a JSON body — the payload. APNS validates the payload: if the JSON is malformed or exceeds the size limit, Apple’s server returns a 400 Bad Request error. After validation, APNS delivers the payload to the device, where iOS parses it and determines how to handle the notification — show a banner, run a background task, or play a sound.
The APNS payload format evolved from a simple text payload in iOS 2 to a multi-component JSON structure in modern versions. iOS 10 introduced support for media attachments via mutable-content, iOS 12 added notification grouping via thread-id, and iOS 15 introduced supports-live-activities for Live Activities. Today, a payload can contain up to 15 different keys depending on the desired notification behavior.
The root object of the payload contains an aps dictionary and optional custom fields at the top level. The aps dictionary is the only mandatory element, but inside it various key combinations can appear depending on the notification type: alert, badge, sound, content-available, mutable-content, interruption-level, and others.
| aps Key | Type | Purpose |
|---|---|---|
| alert | String or Dictionary | Notification text or an object with title, subtitle, body, and localization |
| badge | Number | Number on the app icon; 0 removes the badge |
| sound | String | Sound file name or default for the system sound |
| content-available | Number (1) | Background activation flag; 1 = silent push |
| mutable-content | Number (1) | Flag to activate Service Extension for content modification |
| category | String | Category identifier for buttons and Content Extension |
| thread-id | String | Group identifier for notification grouping |
| interruption-level | String | Interruption level: passive, active, time-sensitive, critical |
| relevance-score | Number (0–1) | Notification priority for the smart ranking system |
The alert key can be a simple string (which becomes the notification body) or a dictionary with title, subtitle, and body fields. The dictionary format allows setting the title and subtitle separately from the main text. For localized notifications, use title-loc-key, title-loc-args, loc-key, and loc-args keys, which reference the app’s Localizable.strings. This allows sending a payload without language-specific text — the app substitutes the translation.
Starting with iOS 15, Apple introduced the Focus Mode mechanism, which requires developers to specify the notification interruption level. interruption-level accepts the following values: passive (no sound, no screen wake), active (standard behavior), time-sensitive (breaks through Focus, requires special entitlement), and critical (medical/emergency situations). The relevance-score key (0–1) helps the Focus system rank notifications within a single category.
The thread-id key groups notifications in the Notification Center. All notifications with the same thread-id are displayed as a single group that the user can expand. This is especially useful for messengers where messages from one contact are grouped together, or for apps that send many notifications of the same type.
Custom fields are any keys outside the aps dictionary that the developer adds to pass additional data to the device. The server includes them in the root JSON object of the payload, and the app retrieves them via userInfo in UNNotificationContent. Custom fields must not duplicate key names from aps to avoid parsing conflicts.
The main limitation is that the total payload size must not exceed 4096 bytes. Custom fields compete for this limit with the mandatory aps keys, so it is important to minimize the size of transmitted data. Use short key names (e.g., “uid” instead of “user-id”), avoid large JSON structures, and pass only identifiers rather than full data objects.
Custom fields come from the server and should not be trusted without verification. Always validate the types and values of custom fields when parsing: check for key existence via optional binding, cast to the expected type using as? String/Int/Dictionary, and handle the nil case. Never use force unwrap (!) for data from the payload — the server may send invalid data, causing the app to crash.
{
"aps": {
"alert": {
"title": "New message",
"body": "Hello! How are you?"
},
"badge": 5,
"sound": "default",
"category": "message",
"thread-id": "chat_4521",
"mutable-content": 1
},
"sender-id": "user_789",
"chat-id": "chat_4521",
"message-type": "text",
"image-url": "https://cdn.example.com/img.jpg"
}
Use a consistent naming style for custom fields across all payloads in the project. kebab-case (message-type) or camelCase (messageType) — both approaches are acceptable, but consistency within the project is important. Avoid long names: “uid” instead of “user-identifier”, “img” instead of “profile-image-url”. Every character in a key name consumes one byte of the 4096 limit.
Different scenarios of push notifications require different key combinations in the payload. Let’s look at several typical examples: a simple text notification, a localized notification, a Silent Push, and a Rich Notification with a media attachment.
A basic payload with text and sound — the minimum configuration to display a notification to the user. Alert as a string provides a short message, sound default plays the standard system sound. Badge is optional and sets the counter on the app icon. category and thread-id are added for grouping and interactivity.
{
"aps": {
"alert": "Reminder: meeting in 15 minutes",
"badge": 3,
"sound": "default"
}
}
To send notifications to devices with different languages, use localization keys instead of hardcoded text. title-loc-key references a key in the app’s Localizable.strings, and title-loc-args supplies arguments. This allows sending a single payload to all devices, while the app displays the text in the appropriate language.
{
"aps": {
"alert": {
"title-loc-key": "NEW_MESSAGE_TITLE",
"title-loc-args": ["Anna"],
"loc-key": "NEW_MESSAGE_BODY",
"loc-args": ["Hello!"]
},
"sound": "message.caf"
}
}
For background synchronization without showing a notification, use content-available: 1 with no alert. Custom fields specify the operation type and data for processing. The system activates the app in the background, calls didReceiveRemoteNotification with fetchCompletionHandler, and the app performs synchronization.
{
"aps": {
"content-available": 1
},
"sync-type": "invalidate-cache",
"timestamp": "2026-07-03T12:00:00Z"
}
To display a media attachment, mutable-content: 1 is needed to activate the Service Extension, along with an image URL in a custom field. mutable-content: 1 signals the system to launch UNNotificationServiceExtension, which downloads the image from the URL and adds it as a UNNotificationAttachment. The category key specifies a registered category for displaying action buttons.
{
"aps": {
"alert": {
"title": "New product",
"body": "Check out the new collection"
},
"category": "product",
"mutable-content": 1
},
"media-url": "https://cdn.example.com/product.jpg"
}
UNNotificationContent.userInfo contains the full dictionary of the received payload after system processing. The app accesses the payload in the UNUserNotificationCenter delegate when receiving a notification (in the foreground), when tapping on a notification, as well as in Service Extension and Content Extension. Correct parsing is essential for extracting custom data and determining the next actions.
When the user taps a notification, the system calls the didReceive response method in UNUserNotificationCenterDelegate. response.notification.request.content.userInfo contains the full payload. The developer extracts custom fields, determines the action type (e.g., open a chat, navigate to a product), and triggers the appropriate navigation in the app.
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification
.request.content.userInfo
guard let chatId = userInfo["chat-id"] as? String
else {
completionHandler()
return
}
let messageType = userInfo["message-type"]
as? String ?? "text"
NavigationRouter.shared.navigate(
to: .chat(chatId: chatId,
messageType: messageType))
completionHandler()
}
The Service Extension receives the payload before the notification is displayed and can modify it. Payload validation is the first step in didReceive: check for mandatory custom fields, validate the attachment URL, and verify data types. If the payload is invalid, call the completion handler with the original content immediately to avoid wasting time on unnecessary processing.
For debugging push notifications in production, use structured payload logging. OSLog allows logging the payload with the “notifications” category at the debug level. On the server side, monitor APNS responses: a successful response contains apns-id for matching with the sent payload, while a 400 error indicates malformed JSON or size exceedance.
Frequently Asked Questions
The maximum payload size is 4096 bytes for regular push notifications and 5120 bytes for VoIP push (PushKit). Exceeding this limit causes APNS to return a 400 Bad Request error. Size is counted in bytes, not characters — account for UTF-8 encoding.
Use the loc-key, title-loc-key, loc-args, and title-loc-args keys inside alert. The app substitutes the translation from its Localizable.strings based on the device language. This allows sending a single payload to all devices regardless of their language.
content-available activates the app in the background for data processing (silent push) without showing a notification. mutable-content activates the Service Extension to modify content before display. Both keys can be used together for background processing and subsequent notification modification.
Use APNS Sandbox for testing and check Apple’s server HTTP response: 200 OK means successful delivery. For structure validation, use JSON schemas in your CI/CD pipeline. In Xcode, send test notifications via the simulator using xcrun simctl push.
apns-id is a unique push notification identifier in the APNS system, returned in the response upon successful delivery. It is used for tracking delivery via the Logs API and for debugging. The server should store apns-id for each sent notification.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also