Push Notifications API Reference
Comprehensive API reference for APNs HTTP/2 transport, UserNotifications framework, and push-driven features including Live Activities and broadcast push.
Quick Reference
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
sendTokenToServer(token)
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Registration failed: \(error)")
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
return [.banner, .sound, .badge]
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
let userInfo = response.notification.request.content.userInfo
}
}
APNs Transport Reference
Endpoints
| Environment |
Host |
Port |
| Development |
api.sandbox.push.apple.com |
443 or 2197 |
| Production |
api.push.apple.com |
443 or 2197 |
Request Format
POST /3/device/{device_token}
Host: api.push.apple.com
Authorization: bearer {jwt_token}
apns-topic: {bundle_id}
apns-push-type: alert
Content-Type: application/json
APNs Headers
| Header |
Required |
Values |
Notes |
| apns-push-type |
Yes |
alert, background, liveactivity, voip, complication, fileprovider, mdm, location |
Must match payload content |
| apns-topic |
Yes |
Bundle ID (or .push-type.liveactivity suffix) |
Required for token-based auth |
| apns-priority |
No |
10 (immediate), 5 (power-conscious), 1 (low) |
Default: 10 for alert, 5 for background |
| apns-expiration |
No |
UNIX timestamp or 0 |
0 = deliver once, don't store |
| apns-collapse-id |
No |
String ≤64 bytes |
Replaces matching notification on device |
| apns-id |
No |
UUID (lowercase) |
Returned by APNs for tracking |
| authorization |
Token auth |
bearer {JWT} |
Not needed for certificate auth |
| apns-unique-id |
Response only |
UUID |
Use with Push Notifications Console delivery log |
Response Codes
| Status |
Meaning |
Common Cause |
| 200 |
Success |
|
| 400 |
Bad request |
Malformed JSON, missing required header |
| 403 |
Forbidden |
Expired JWT, wrong team/key, topic mismatch |
| 404 |
Not found |
Invalid device token path |
| 405 |
Method not allowed |
Not using POST |
| 410 |
Unregistered |
Device token no longer active (app uninstalled) |
| 413 |
Payload too large |
Exceeds 4KB (5KB for VoIP) |
| 429 |
Too many requests |
Rate limited by APNs |
| 500 |
Internal server error |
APNs issue, retry |
| 503 |
Service unavailable |
APNs overloaded, retry with backoff |
JWT Authentication Reference
JWT Header
{ "alg": "ES256", "kid": "{10-char Key ID}" }
JWT Claims
{ "iss": "{10-char Team ID}", "iat": {unix_timestamp} }
Rules
| Rule |
Detail |
| Algorithm |
ES256 (P-256 curve) |
| Signing key |
APNs auth key (.p8 from developer portal) |
| Token lifetime |
Max 1 hour (403 ExpiredProviderToken if older) |
| Refresh interval |
Between 20 and 60 minutes |
| Scope |
One key works for all apps in team, both environments |
Authorization Header Format
authorization: bearer eyAia2lkIjog...
Payload Reference
aps Dictionary Keys
| Key |
Type |
Purpose |
Since |
| alert |
Dict/String |
Alert content |
iOS 10 |
| badge |
Number |
App icon badge (0 removes) |
iOS 10 |
| sound |
String/Dict |
Audio playback |
iOS 10 |
| thread-id |
String |
Notification grouping |
iOS 10 |
| category |
String |
Actionable notification type |
iOS 10 |
| content-available |
Number (1) |
Silent background push |
iOS 10 |
| mutable-content |
Number (1) |
Triggers service extension |
iOS 10 |
| target-content-id |
String |
Window/content identifier |
iOS 13 |
| interruption-level |
String |
passive/active/time-sensitive/critical |
iOS 15 |
| relevance-score |
Number 0-1 |
Notification summary sorting |
iOS 15 |
| filter-criteria |
String |
Focus filter matching |
iOS 15 |
| stale-date |
Number |
UNIX timestamp (Live Activity) |
iOS 16.1 |
| content-state |
Dict |
Live Activity content update |
iOS 16.1 |
| timestamp |
Number |
UNIX timestamp (Live Activity) |
iOS 16.1 |
| event |
String |
start/update/end (Live Activity) |
iOS 16.1 |
| dismissal-date |
Number |
UNIX timestamp (Live Activity) |
iOS 16.1 |
| attributes-type |
String |
Live Activity struct name |
iOS 17 |
| attributes |
Dict |
Live Activity init data |
iOS 17 |
Alert Dictionary Keys
| Key |
Type |
Purpose |
| title |
String |
Short title |
| subtitle |
String |
Secondary description |
| body |
String |
Full message |
| launch-image |
String |
Launch screen filename |
| title-loc-key |
String |
Localization key for title |
| title-loc-args |
[String] |
Title format arguments |
| subtitle-loc-key |
String |
Localization key for subtitle |
| subtitle-loc-args |
[String] |
Subtitle format arguments |
| loc-key |
String |
Localization key for body |
| loc-args |
[String] |
Body format arguments |
Sound Dictionary (Critical Alerts)
{ "critical": 1, "name": "alarm.aiff", "volume": 0.8 }
Interruption Level Values
| Value |
Behavior |
Requires |
| passive |
No sound/wake. Notification summary only. |
Nothing |
| active |
Default. Sound + banner. |
Nothing |
| time-sensitive |
Breaks scheduled delivery. Banner persists. |
Time Sensitive capability |
| critical |
Overrides DND and ringer switch. |
Apple approval + entitlement |
Example Payloads
Basic Alert
{
"aps": {
"alert": {
"title": "New Message",
"subtitle": "From Alice",
"body": "Hey, are you free for lunch?"
},
"badge": 3,
"sound": "default"
}
}
Localized with loc-key/loc-args
{
"aps": {
"alert": {
"title-loc-key": "MESSAGE_TITLE",
"title-loc-args": ["Alice"],
"loc-key": "MESSAGE_BODY",
"loc-args": ["Alice", "lunch"]
},
"sound": "default"
}
}
Silent Background Push
{
"aps": {
"content-available": 1
},
"custom-key": "sync-update"
}
Rich Notification (Service Extension)
{
"aps": {
"alert": {
"title": "Photo shared",
"body": "Alice shared a photo with you"
},
"mutable-content": 1,
"sound": "default"
},
"image-url": "https://example.com/photo.jpg"
}
Critical Alert
{
"aps": {
"alert": {
"title": "Server Down",
"body": "Production database is unreachable"
},
"sound": { "critical": 1, "name": "default", "volume": 1.0 },
"interruption-level": "critical"
}
}
Time-Sensitive with Category
{
"aps": {
"alert": {
"title": "Package Delivered",
"body": "Your order has been delivered to the front door"
},
"interruption-level": "time-sensitive",
"category": "DELIVERY",
"sound": "default"
},
"order-id": "12345"
}
UNUserNotificationCenter API Reference
Key Methods
| Method |
Purpose |
| requestAuthorization(options:) |
Request permission |
| notificationSettings() |
Check current status |
| add(_:) |
Schedule notification request |
| getPendingNotificationRequests() |
List scheduled |
| removePendingNotificationRequests(withIdentifiers:) |
Cancel scheduled |
| getDeliveredNotifications() |
List in notification center |
| removeDeliveredNotifications(withIdentifiers:) |
Remove from center |
| setNotificationCategories(_:) |
Register actionable types |
| setBadgeCount(_:) |
Update badge (iOS 16+) |
| supportsContentExtensions |
Check content extension support |
UNAuthorizationOptions
| Option |
Purpose |
| .alert |
Display alerts |
| .badge |
Update badge count |
| .sound |
Play sounds |
| .carPlay |
Show in CarPlay |
| .criticalAlert |
Critical alerts (requires entitlement) |
| .provisional |
Trial delivery without prompting |
| .providesAppNotificationSettings |
"Configure in App" button in Settings |
| .announcement |
Siri announcement (deprecated iOS 15+) |
UNAuthorizationStatus
| Value |
Meaning |
| .notDetermined |
No prompt shown yet |
| .denied |
User denied or disabled in Settings |
| .authorized |
User explicitly granted |
| .provisional |
Provisional trial delivery |
| .ephemeral |
App Clip temporary |
Request Authorization
let center = UNUserNotificationCenter.current()
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
if granted {
await MainActor.run {
UIApplication.shared.registerForRemoteNotifications()
}
}
Check Settings
let settings = await center.notificationSettings()
switch settings.authorizationStatus {
case .authorized: break
case .denied:
case .provisional:
case .notDetermined:
case .ephemeral:
@unknown default: break
}
Delegate Methods
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async
-> UNNotificationPresentationOptions {
return [.banner, .sound, .badge]
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
let actionIdentifier = response.actionIdentifier
let userInfo = response.notification.request.content.userInfo
switch actionIdentifier {
case UNNotificationDefaultActionIdentifier:
break
case UNNotificationDismissActionIdentifier:
break
default:
break
}
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
openSettingsFor notification: UNNotification?) {
}
UNNotificationCategory and UNNotificationAction API
Category Registration
let likeAction = UNNotificationAction(
identifier: "LIKE",
title: "Like",
options: []
)
let replyAction = UNTextInputNotificationAction(
identifier: "REPLY",
title: "Reply",
options: [],
textInputButtonTitle: "Send",
textInputPlaceholder: "Type a message..."
)
let deleteAction = UNNotificationAction(
identifier: "DELETE",
title: "Delete",
options: [.destructive, .authenticationRequired]
)
let messageCategory = UNNotificationCategory(
identifier: "MESSAGE",
actions: [likeAction, replyAction, deleteAction],
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: "New message",
categorySummaryFormat: "%u more messages",
options: [.customDismissAction]
)
UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
Action Options
| Option |
Effect |
| .authenticationRequired |
Requires device unlock |
| .destructive |
Red text display |
| .foreground |
Launches app to foreground |
Category Options
| Option |
Effect |
| .customDismissAction |
Fires delegate on dismiss |
| .allowInCarPlay |
Show actions in CarPlay |
| .hiddenPreviewsShowTitle |
Show title when previews hidden |
| .hiddenPreviewsShowSubtitle |
Show subtitle when previews hidden |
| .allowAnnouncement |
Siri can announce (deprecated iOS 15+) |
UNNotificationActionIcon (iOS 15+)
let icon = UNNotificationActionIcon(systemImageName: "hand.thumbsup")
let action = UNNotificationAction(
identifier: "LIKE",
title: "Like",
options: [],
icon: icon
)
UNNotificationServiceExtension API
Modifies notification content before display. Runs in a separate extension process.
Lifecycle
| Method |
Window |
Purpose |
| didReceive(_:withContentHandler:) |
~30 seconds |
Modify notification content |
| serviceExtensionTimeWillExpire() |
Called at deadline |
Deliver best attempt immediately |
Implementation
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
guard let content = bestAttemptContent,
let imageURLString = content.userInfo["image-url"] as? String,
let imageURL = URL(string: imageURLString) else {
contentHandler(request.content)
return
}
let task = URLSession.shared.downloadTask(with: imageURL) { url, _, error in
defer { contentHandler(content) }
guard let url = url, error == nil else { return }
let attachment = try? UNNotificationAttachment(
identifier: "image",
url: url,
options: [UNNotificationAttachmentOptionsTypeHintKey: "public.jpeg"]
)
if let attachment = attachment {
content.attachments = [attachment]
}
}
task.resume()
}
override func serviceExtensionTimeWillExpire() {
if let content = bestAttemptContent {
contentHandler?(content)
}
}
}
Supported Attachment Types
| Type |
Extensions |
Max Size |
| Image |
.jpg, .gif, .png |
10 MB |
| Audio |
.aif, .wav, .mp3 |
5 MB |
| Video |
.mp4, .mpeg |
50 MB |
Payload Requirement
The notification payload must include "mutable-content": 1 in the aps dictionary for the service extension to fire.
Local Notifications API
Trigger Types
| Trigger |
Use Case |
Repeating |
| UNTimeIntervalNotificationTrigger |
After N seconds |
Yes (≥60s) |
| UNCalendarNotificationTrigger |
Specific date/time |
Yes |
| UNLocationNotificationTrigger |
Enter/exit region |
Yes |
Time Interval Trigger
let content = UNMutableNotificationContent()
content.title = "Reminder"
content.body = "Time to take a break"
content.sound = .default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 300, repeats: false)
let request = UNNotificationRequest(
identifier: "break-reminder",
content: content,
trigger: trigger
)
try await UNUserNotificationCenter.current().add(request)
Calendar Trigger
var dateComponents = DateComponents()
dateComponents.hour = 9
dateComponents.minute = 0
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
let request = UNNotificationRequest(
identifier: "daily-9am",
content: content,
trigger: trigger
)
try await UNUserNotificationCenter.current().add(request)
Location Trigger
import CoreLocation
let center = CLLocationCoordinate2D(latitude: 37.3349, longitude: -122.0090)
let region = CLCircularRegion(center: center, radius: 100, identifier: "apple-park")
region.notifyOnEntry = true
region.notifyOnExit = false
let trigger = UNLocationNotificationTrigger(region: region, repeats: false)
let request = UNNotificationRequest(
identifier: "arrived-at-office",
content: content,
trigger: trigger
)
try await UNUserNotificationCenter.current().add(request)
Limitations
| Limitation |
Detail |
| Minimum repeat interval |
60 seconds for UNTimeIntervalNotificationTrigger |
| Location authorization |
Location trigger requires When In Use or Always authorization |
| No service extensions |
Local notifications do not trigger UNNotificationServiceExtension |
| No background wake |
Local notifications cannot use content-available for background processing |
| App extensions |
Local notifications cannot be scheduled from app extensions (use app group + main app) |
| Pending limit |
64 pending notification requests per app |
Live Activity Push Headers
Required Headers
| Header |
Value |
| apns-push-type |
liveactivity |
| apns-topic |
{bundleID}.push-type.liveactivity |
| apns-priority |
5 (routine) or 10 (time-sensitive) |
Event Types
| Event |
Purpose |
Required Fields |
| start |
Start Live Activity remotely |
attributes-type, attributes, content-state, timestamp |
| update |
Update content |
content-state, timestamp |
| end |
End Live Activity |
timestamp (content-state optional) |
Update Payload
{
"aps": {
"timestamp": 1709913600,
"event": "update",
"content-state": {
"homeScore": 2,
"awayScore": 1,
"inning": "Top 7"
}
}
}
Start Payload (Push-to-Start Token)
{
"aps": {
"timestamp": 1709913600,
"event": "start",
"content-state": {
"homeScore": 0,
"awayScore": 0,
"inning": "Top 1"
},
"attributes-type": "GameAttributes",
"attributes": {
"homeTeam": "Giants",
"awayTeam": "Dodgers"
},
"alert": {
"title": "Game Starting",
"body": "Giants vs Dodgers is about to begin"
}
}
}
Start Payload (Channel-Based)
{
"aps": {
"timestamp": 1709913600,
"event": "start",
"content-state": {
"homeScore": 0,
"awayScore": 0,
"inning": "Top 1"
},
"attributes-type": "GameAttributes",
"attributes": {
"homeTeam": "Giants",
"awayTeam": "Dodgers"
}
}
}
End Payload
{
"aps": {
"timestamp": 1709913600,
"event": "end",
"dismissal-date": 1709917200,
"content-state": {
"homeScore": 5,
"awayScore": 3,
"inning": "Final"
}
}
}
Push-to-Start Token
for await token in Activity<GameAttributes>.pushToStartTokenUpdates {
let tokenString = token.map { String(format: "%02x", $0) }.joined()
sendPushToStartTokenToServer(tokenString)
}
Activity Push Token
for await tokenData in activity.pushTokenUpdates {
let token = tokenData.map { String(format: "%02x", $0) }.joined()
sendActivityTokenToServer(token, activityId: activity.id)
}
Content-state encoding rule: the system always uses default JSONDecoder — do not use custom encoding strategies in your ActivityAttributes.ContentState.
Broadcast Push API (iOS 18+)
Server-to-many push for Live Activities without tracking individual device tokens.
Endpoint
POST /4/broadcasts/apps/{TOPIC}
Headers
| Header |
Value |
| apns-push-type |
liveactivity |
| apns-channel-id |
{channelID} |
| authorization |
bearer {JWT} |
Subscribe via Channel
try Activity.request(
attributes: attributes,
content: .init(state: initialState, staleDate: nil),
pushType: .channel(channelId)
)
Channel Storage Policies
| Policy |
Behavior |
Budget |
| No Storage |
Deliver only to connected devices |
Higher |
| Most Recent Message |
Store latest for offline devices |
Lower |
Command-Line Testing
JWT Generation
JWT_ISSUE_TIME=$(date +%s)
JWT_HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
JWT_CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${JWT_ISSUE_TIME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
JWT_HEADER_CLAIMS="${JWT_HEADER}.${JWT_CLAIMS}"
JWT_SIGNED_HEADER_CLAIMS=$(printf "${JWT_HEADER_CLAIMS}" | openssl dgst -binary -sha256 -sign "${TOKEN_KEY_FILE_NAME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
AUTHENTICATION_TOKEN="${JWT_HEADER}.${JWT_CLAIMS}.${JWT_SIGNED_HEADER_CLAIMS}"
Send Alert Push
curl -v \
--header "apns-topic: $TOPIC" \
--header "apns-push-type: alert" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data '{"aps":{"alert":"test"}}' \
--http2 https://${APNS_HOST_NAME}/3/device/${DEVICE_TOKEN}
Send Live Activity Push
curl \
--header "apns-topic: com.example.app.push-type.liveactivity" \
--header "apns-push-type: liveactivity" \
--header "apns-priority: 10" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data '{
"aps": {
"timestamp": '$(date +%s)',
"event": "update",
"content-state": { "score": "2-1" }
}
}' \
--http2 https://api.sandbox.push.apple.com/3/device/$ACTIVITY_PUSH_TOKEN
Simulator Push
xcrun simctl push booted com.example.app payload.json
Simulator Payload File
{
"Simulator Target Bundle": "com.example.app",
"aps": {
"alert": { "title": "Test", "body": "Hello" },
"sound": "default"
}
}
Resources
WWDC: 2021-10091, 2023-10025, 2023-10185, 2024-10069
Docs: /usernotifications, /usernotifications/sending-notification-requests-to-apns, /usernotifications/generating-a-remote-notification, /activitykit
Skills: axiom-push-notifications, axiom-push-notifications-diag, axiom-extensions-widgets