Learn how iOS push notifications and device tokens work, why token management is the hard part, and how Courier's iOS SDK syncs tokens so you send to a user.
Updated Jul 6, 2026
Push notifications are how an iOS app reaches a user when they are not in the app: a message on the lock screen or in Notification Center, or a silent update handled in the background. Used well, they drive engagement and deliver timely, personalized alerts, an order that shipped, a payment that failed, a reminder that a task is due.
Sending one push is easy. Running push reliably in production is not, and the part that surprises most teams is device token management: every device has a token, tokens rotate, reinstalls invalidate them, and you have to keep them mapped to the right user across every device they own.
This guide explains how iOS push notifications work, why token management is the hard part, and how Courier handles that for you so you can send to a user instead of juggling raw device tokens.
Every iOS push is delivered by Apple through the Apple Push Notification service (APNs). The flow is always the same:
You enable the Push Notifications capability in Xcode under Signing & Capabilities (this requires a paid Apple Developer account), then request permission and register:
import UIKitimport UserNotifications@mainclass AppDelegate: UIResponder, UIApplicationDelegate {func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ inguard granted else { return }DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() }}return true}func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {// This APNs device token has to be stored and mapped to the signed-in user.}}
Many teams also use Firebase Cloud Messaging (FCM) on top of APNs so one integration covers iOS and Android (on iOS, FCM still delivers through APNs). Either way, the delivery mechanics are the same, and so is the hard part below.
The didRegisterForRemoteNotificationsWithDeviceToken callback hands you a token, but a token is not a user. Turning "I have a token" into "I can reliably reach this user" is where the real work lives:
token -> user, update it on every rotation, remove stale tokens, and keep it in sync as users sign in and out.None of this is about sending a message. It is bookkeeping, and getting it wrong is why "some users stopped getting notifications" is such a common bug. This is exactly what Courier takes off your plate.
Courier sits between your app and the push providers. Its native iOS SDK (Courier_iOS) syncs and maintains device tokens for the signed-in user automatically, and its Send API lets your backend send to a user_id across APNs, FCM, and other channels. You stop managing tokens and start addressing users.
Install Courier_iOS with Swift Package Manager (https://github.com/trycourier/courier-ios) or CocoaPods:
platform :ios, '15.0'target 'YourApp' dopod 'Courier_iOS'end
Courier authenticates each user with a short-lived JWT you mint on your backend with the Issue Token endpoint. Include the write:user-tokens scope so the SDK can sync device tokens.
curl -X POST https://api.courier.com/auth/issue-token \-H "Authorization: Bearer $COURIER_API_KEY" \-H "Content-Type: application/json" \-d '{"scope":"user_id:user_123 write:user-tokens inbox:read:messages inbox:write:events","expires_in":"2 days"}'
import Courier_iOSTask {await Courier.shared.signIn(userId: "user_123", accessToken: jwt)}
Make your AppDelegate subclass CourierDelegate. It registers for remote notifications and syncs the APNs token to the signed-in user automatically, including on rotation, so the token-management work above is handled for you.
import Courier_iOS@mainclass AppDelegate: CourierDelegate {override func pushNotificationDeliveredInForeground(message: [AnyHashable: Any]) -> UNNotificationPresentationOptions {return [.sound, .list, .banner, .badge]}override func pushNotificationClicked(message: [AnyHashable: Any]) {print(message)}}
Request permission when it fits your flow, and sync other providers (like FCM) with one call:
let status = try await Courier.requestNotificationPermission()try await Courier.shared.setToken(for: .firebaseFcm, token: fcmToken)
Here is what the SDK is doing so you do not have to:
CourierDelegate captures the APNs token and keeps it current for the signed-in user_id; setToken(for:token:) does the same for FCM and other providers.user_id.user_id and Courier resolves the devices, with delivery tracked in Message Logs.Because tokens are synced to the user, sending is a single call to a user_id. Use inline content or a template you built in Design Studio:
curl -X POST https://api.courier.com/send \-H "Authorization: Bearer $COURIER_API_KEY" \-H "Content-Type: application/json" \-d '{"message": {"to": { "user_id": "user_123" },"content": { "title": "Your order shipped", "body": "Track it anytime in the app." },"routing": { "method": "all", "channels": ["apn"] },"providers": {"apn": { "override": { "body": { "aps": { "sound": "default", "badge": 1 } } } }}}}'
import Courier from "@trycourier/courier";const client = new Courier({ apiKey: process.env.COURIER_API_KEY });const { requestId } = await client.send.message({message: {to: { user_id: "user_123" },content: { title: "Your order shipped", body: "Track it anytime in the app." },routing: { method: "all", channels: ["apn"] },},});
import osfrom courier import Courierclient = Courier(api_key=os.environ["COURIER_API_KEY"])response = client.send.message(message={"to": {"user_id": "user_123"},"content": {"title": "Your order shipped", "body": "Track it anytime in the app."},"routing": {"method": "all", "channels": ["apn"]},})print(response.request_id)
The same SDK also ships a prebuilt in-app inbox (CourierInbox), so a push and its in-app copy can come from one send. See the Inbox overview.
Do I need Firebase to send push on iOS? No. iOS push is delivered through APNs, so you can send with APNs alone. FCM is optional and, on iOS, still delivers via APNs. Teams add FCM mainly to share one setup across iOS and Android.
Why is device token management such a problem?
Tokens rotate, reinstalls invalidate them, one user has several devices, and you must keep token -> user in sync and prune dead tokens. Miss any of that and some users silently stop receiving notifications. Courier's SDK syncs and maintains tokens for you.
How does Courier let me send to a user instead of a token?
The Courier iOS SDK syncs each device's token to the signed-in user_id. Your backend then sends to that user_id and Courier delivers to every device registered for the user.
Can one notification reach a user across all their devices?
Yes. Send to a user_id and Courier delivers to every synced device (for example an iPhone and an iPad).
Do I still need a paid Apple Developer account? Yes. The Push Notifications capability and APNs keys require a paid Apple Developer Program membership, whether you send through APNs directly, FCM, or Courier.
How do I test push on iOS? Use a physical device. Simulators are unreliable for registering tokens and receiving remote push, so verify token sync and delivery on real hardware.
Push notifications keep iOS users engaged, but the durable challenge is not sending, it is keeping device tokens mapped to users as they rotate, reinstall, and add devices. Courier handles that with a native iOS SDK that syncs tokens automatically and a Send API that lets you address a user_id across APNs, FCM, and in-app inbox from one call. You focus on what to send; Courier keeps track of where it goes.
Keep exploring
One API, every channel
Courier gives you one API for email, SMS, push, and chat, with templates, routing, retries, and delivery logs built in.
Last updated Jul 6, 2026. Code samples are illustrative; provider APIs and pricing change over time, so check each provider’s docs before relying on them.
© 2026 Courier. All rights reserved.