Native app SDKs
This page is about a different feature from the rest of these docs: if you have your own native iOS or Android app (not just a website) and want to push notifications to it, these SDKs wrap the device-token REST API for you — install, registration, rotation, notification display, and tap handling all included. (Looking for how web push works on a phone's browser instead? That's web push on mobile — different feature, no app required on either side.)
One-time setup either way: upload your APNs key (iOS) and/or Firebase service account JSON
(Android) in the dashboard under Site → Mobile push. Registration is
accepted before that's done — a token is never lost to setup-order timing — but sends won't
deliver until credentials are in place. Mobile deliveries draw on the same monthly quota as
web: no separate plan, no second counter, and the same POST /notifications call
reaches web and native-app audiences together via platforms or
device_tokens — see mobile device
tokens in the REST API reference for the raw endpoints both SDKs below call under the
hood.
iOS
PushNotificationServiceSDK — MIT, Swift Package, min iOS 15, zero third-party dependencies.
Xcode → File → Add Package Dependencies →
https://github.com/pushnotificationlabs/pushnotificationservice-ios
Register for push
Requires the Xcode Push Notifications capability on your app target
(Signing & Capabilities → + Capability → Push Notifications) — without it,
registerForRemoteNotifications() fails silently.
import UIKit
import PushNotificationServiceSDK
import UserNotifications
// AppDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
PushNotificationService.configure(siteId: "YOUR_SITE_ID")
UNUserNotificationCenter.current().delegate = self
Task {
// Without this, registerForRemoteNotifications() below still
// produces a device token, but the app is never authorized to
// actually display anything — silently.
let granted = try? await UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge])
if granted == true {
application.registerForRemoteNotifications()
}
}
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Task { try? await PushNotificationService.didReceiveToken(deviceToken) }
}
// UNUserNotificationCenterDelegate
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
await PushNotificationService.willPresent(notification)
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
await PushNotificationService.didReceive(response)
}
}
Rich images & reliable display receipts (recommended)
An image attachment and a display receipt ping both require a small Notification Service Extension:
- Xcode → File → New → Target → Notification Service Extension.
- Add the
PushNotificationServiceExtensionKitproduct to that new target. - Replace the generated
NotificationService.swiftwith:
import PushNotificationServiceExtensionKit
final class NotificationService: PushNotificationServiceExtension {}
Best-effort: iOS can skip service extensions under memory pressure or Low Power Mode. Without it, notifications still display and are still tappable — you just won't get the image or a receipt ping while the app is backgrounded.
Tap handling & unregister
By default, tapping a notification opens its (tracking) URL via the OS. To intercept:
PushNotificationService.onNotificationTapped = { url in
// your own routing — invoked off the main thread; hop to @MainActor for UI work
}
// e.g. on logout
Task { try? await PushNotificationService.unregister() }
Full reference, including error handling (PushNotificationServiceError's
cases), lives in the
repo's own README.
Android
com.pushnotificationservice:android-sdk on Maven Central — MIT, Kotlin, min SDK 23. No bundled networking/JSON/Firebase dependency; it only ever receives plain strings from the Firebase Messaging you already have.
// build.gradle.kts
dependencies {
implementation("com.pushnotificationservice:android-sdk:0.1.2")
}
Register for push
// Application.onCreate()
PushNotificationService.configure(applicationContext, siteId = "YOUR_SITE_ID")
// Your FirebaseMessagingService subclass
override fun onNewToken(token: String) {
CoroutineScope(Dispatchers.IO).launch {
runCatching { PushNotificationService.onNewToken(token) }
}
}
override fun onMessageReceived(message: RemoteMessage) {
PushNotificationService.onMessageReceived(
context = applicationContext,
title = message.notification?.title,
body = message.notification?.body,
image = message.notification?.imageUrl?.toString(),
data = message.data,
)
}
This only runs while the app is foregrounded — a backgrounded app has its notification auto-displayed by the OS directly, with no app code (and so no display receipt) running. Permanent FCM/Android platform limitation, not a gap in the SDK.
On API 33+, posting a notification also needs the runtime
android.permission.POST_NOTIFICATIONS permission — request it yourself (e.g.
via ActivityCompat.requestPermissions) before onMessageReceived
will actually post anything; without it, the call silently no-ops.
Tap handling & unregister
// The Activity your notification's PendingIntent launches
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PushNotificationService.onNotificationOpened(this, intent)
}
Opens the tapped notification's URL via ACTION_VIEW by default. To intercept:
PushNotificationService.onNotificationTapped = { uri ->
// your own routing
}
// e.g. on logout
CoroutineScope(Dispatchers.IO).launch { runCatching { PushNotificationService.unregister() } }
Customize the notification channel (name shown in system settings, importance) before the first message arrives, or the SDK creates a default one on first use:
PushNotificationChannel.configureChannel(name = "Deals", importance = NotificationManager.IMPORTANCE_HIGH)
Full reference lives in the repo's own README.