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
// AppDelegate — minimal remote notification setup 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)")
}
// Show notifications when app is in foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
return [.banner, .sound, .badge]
}
// Handle notification tap / action response
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
let userInfo = response.notification.request.content.userInfo
// Route to appropriate screen based on 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: // Direct user to Settings case .provisional: // Upgrade to full authorization case .notDetermined: // Request authorization case .ephemeral: // App Clip — temporary @unknown default: break }
Delegate Methods
// Foreground presentation — called when notification arrives while app is active func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { return [.banner, .sound, .badge] }
// Action response — called when user taps notification or action button func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { let actionIdentifier = response.actionIdentifier let userInfo = response.notification.request.content.userInfo
switch actionIdentifier {
case UNNotificationDefaultActionIdentifier:
// User tapped notification body
break
case UNNotificationDismissActionIdentifier:
// User dismissed (requires .customDismissAction on category)
break
default:
// Custom action
break
}
}
// Settings — called when user taps "Configure in App" from notification settings func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) { // Navigate to in-app notification settings }
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
}
// Download and attach image
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
// Observe push-to-start tokens (iOS 17.2+) for await token in Activity<GameAttributes>.pushToStartTokenUpdates { let tokenString = token.map { String(format: "%02x", $0) }.joined() sendPushToStartTokenToServer(tokenString) }
Activity Push Token
// Observe activity-specific push tokens 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