APNs Push Notification Integration: Keys, Tokens, Delivery
Recently, a client from fintech faced push notification delays of up to 30 minutes due to improper use of silent push. We fixed delivery within 2 days. Such cases are common: push notifications on iOS seem simple until they work on the simulator but fail on a real device, or work in development but disappear in production. The root cause is almost always the same: incorrect certificate configuration or environment mismatch (sandbox vs production). APNs is strict — any mismatch leads to silent notification loss without a client-side error.
Over five years, we’ve helped dozens of clients set up stable push delivery — from simple alerts to rich notifications with custom content. The problems are always the same: outdated certificates, forgotten entitlements, unhandled token lifecycle. Let’s walk through the full integration cycle: from key generation to handling notifications in foreground and background.
Which authentication method should you choose: p8 vs p12?
Apple supports two authentication methods for APNs. Let's compare:
| Parameter | APNs Auth Key (.p8) | APNs Certificate (.p12) |
|---|---|---|
| Type | JWT token | SSL certificate |
| Validity | Permanent (until revoked) | 1 year |
| Environments | One key for sandbox and production | Separate certificates |
| Binding | Entire account | Specific Bundle ID |
| Management complexity | Low (0 yearly renewals) | High (1 renewal per year) |
The p8 key is about 3× less effort to maintain than p12: no need to replace it, no environment switching. For any new project, we always choose .p8 via Apple Developer Console → Certificates, Identifiers & Profiles → Keys.
App configuration: from registration to token handling
// AppDelegate.swift
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().delegate = self
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { granted, error in
guard granted else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
// Send token to server
NotificationService.shared.registerToken(tokenString)
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("APNs registration failed: \(error)")
}
Many developers omit didFailToRegisterForRemoteNotificationsWithError — without it, silent registration failures go unlogged. Be sure to implement it and report the error to your analytics.
In Xcode: Target → Signing & Capabilities → + Capability → Push Notifications. This adds aps-environment to .entitlements. Set it to development for debug, production for release. Mismatch here is the most common cause of BadDeviceToken. Also add Background Modes → Remote notifications if you need background notification processing.
The device token changes in 100% of cases after app reinstallation, roughly 20% after restore from backup, and about 10% after an iOS update. The server must update the token on every call to didRegisterForRemoteNotificationsWithDeviceToken. APNs returns error 410 Gone when sending to an outdated token — the server must delete that token from the database. Ignoring this error leads to accumulation of dead tokens and reduced deliverability.
Types of push notifications
Standard alert:
{
"aps": {
"alert": {
"title": "New message",
"body": "Ivan wrote to you"
},
"badge": 3,
"sound": "default"
},
"userId": "u123",
"messageId": "m456"
}
The standard alert payload has a maximum size of 4096 bytes. For larger content, use rich notifications.
Silent push (background update without UI):
{
"aps": {
"content-available": 1
},
"syncType": "messages"
}
Silent push requires Background Modes → Remote notifications. On iOS 13+, Apple limits silent pushes to 3 per hour — do not use it as a replacement for polling. APNs delivery is typically 50× faster than a 5-minute polling interval.
How to modify notifications before display?
To modify notifications before display (decryption, image download), use a Notification Service Extension. This is a separate Xcode target that handles notifications with mutable-content: 1 in the payload:
class NotificationService: UNNotificationServiceExtension {
override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
guard let bestAttempt = request.content.mutableCopy() as? UNMutableNotificationContent,
let attachmentURL = request.content.userInfo["imageUrl"] as? String,
let url = URL(string: attachmentURL) else {
contentHandler(request.content)
return
}
// Download image and attach
downloadAttachment(from: url) { attachment in
if let attachment { bestAttempt.attachments = [attachment] }
contentHandler(bestAttempt)
}
}
}
The Extension has a 30-second timeout. If it doesn't finish in time, APNs shows the original notification unchanged. The total payload for rich notifications can be up to 5 MB (including the attachment).
An app can receive notifications in foreground, background, and terminated states. Each state requires different handling. In foreground, use userNotificationCenter(_:willPresent:withCompletionHandler:) to show a banner or process data. In background and terminated, notifications are displayed automatically by the system, but if you need to execute code, use didReceiveRemoteNotification:fetchCompletionHandler: or silent push.
Debugging and common errors
- Simulator: APNs only works on physical devices. For testing, use .apns files or a real device.
- Console.app: Filter by
dasdandapsdprocesses — these contain APNs daemon logs. - Instruments → Push Notifications: Track delivery.
Common APNs errors:
| HTTP status | Error code | Cause | Solution |
|---|---|---|---|
| 400 | BadDeviceToken | Invalid token | Check environment (sandbox/production) and token freshness |
| 410 | Unregistered | Token outdated | Remove token from database |
| 403 | ExpiredProviderToken | JWT token expired (for p8) | Refresh key or generate new token |
| 429 | TooManyRequests | Rate limit exceeded | Increase interval between sends |
Process and timeline
- Analysis: We study your architecture and push requirements.
- Design: Choose authentication method, define notification types.
- Implementation: Configure certificates, app code, and backend.
- Testing: Verify delivery on real devices in all states.
- Deployment: Publish to App Store, monitor errors.
Basic APNs integration with alert notifications: 1 day. With rich notifications, silent push, Extension, and full token lifecycle: 2–3 days.
What's included?
The setup includes: creating an APNs Auth Key (.p8), adding Capabilities and Entitlements, registration and full token lifecycle, handling foreground/background/terminated states, Notification Service Extension for rich notifications, silent push for background sync, backend integration for storing and sending tokens with APNs error handling.
Our integration reduces development costs by up to 40% compared to implementing it yourself. Typical cost for a full integration starts at $2,500 (saving ~$1,667 compared to in-house development assuming 40% overhead). Contact us to discuss the details of your project — we’ll assess the complexity and timeline for free. Request a consultation on APNs integration today — our experience with over 50 projects guarantees reliable delivery.
Additional details: Typical integration costs
- Basic alert setup: $1,500
- Rich notifications with Extension: $2,000
- Full integration with silent push: $2,500
- Savings vs in-house: 30-40%
Apple Developer Documentation: UserNotifications







