P Push Notification Service

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:

  1. Xcode → File → New → Target → Notification Service Extension.
  2. Add the PushNotificationServiceExtensionKit product to that new target.
  3. Replace the generated NotificationService.swift with:
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.