Push Notifications
Send targeted push notifications to your iOS and Android users — using your own Firebase and Apple APNs credentials. GatiFlow never shares a notification pipeline across customers.
How it works
GatiFlow acts as an orchestration layer between your dashboard and Apple/Google push infrastructure:
You upload credentials
Add your Firebase service account JSON (Android/FCM) and your APNs .p8 key (iOS) in App Settings. Credentials are stored encrypted and never sent to the client.
Your app registers tokens
The SDK calls GatiFlow's registration endpoint on startup with the FCM token (Android) or APNs token (iOS). Tokens are stored per-app and per-device.
You create a campaign
From the Push Notifications dashboard you compose a title + body + optional data payload, then click Send.
GatiFlow delivers
GatiFlow loads your stored credentials, signs the request with your Firebase service account or APNs key, and sends directly to FCM / APNs. Invalid tokens are automatically disabled.
Why your own credentials? FCM tokens are tied to the Firebase project they were registered in. Using a shared GatiFlow Firebase project would mean tokens from yourproject can't be targeted. Uploading your own service account keeps you in full control — you can revoke access at any time.
Dashboard setup
Before your app can receive push notifications you must upload credentials for your target platform(s). Navigate to Apps → [Your App] → Settings in the GatiFlow dashboard.
Android — Firebase service account
- Open the Firebase Console and select your project.
- Go to Project settings → Service accounts.
- Click Generate new private key and download the JSON file.
- Back in GatiFlow, open App Settings → Firebase (FCM) Credentials.
- Upload the downloaded JSON file or paste its contents.
- Click Save Firebase credentials. GatiFlow validates the JSON before saving.
The service account must have the Firebase Cloud Messaging Admin role (automatically included when you generate a key from the Firebase Console service accounts page).
iOS — APNs Auth Key (.p8)
- Log in to Apple Developer → Certificates, Identifiers & Profiles → Keys.
- Create a new key, enable Apple Push Notifications service (APNs), and download the
.p8file. - Note the Key ID (10-character string shown on the download page) and your Team ID (found in the top-right corner of the Developer portal).
- In GatiFlow App Settings → APNs Credentials, upload the
.p8file, and fill in Key ID and Team ID. - Click Save APNs credentials.
APNs auth keys (as opposed to certificates) never expire and work for all your apps in the same team. GatiFlow uses JWT-based APNs auth — no annual certificate renewal needed.
Android SDK setup
The Android push integration uses Firebase Messaging. You need to add the Firebase dependency, create a service that extends FirebaseMessagingService, and register the GatiFlow PushService.
Step 1 — Add Firebase Messaging
Make sure your app-level build.gradle.kts includes:
dependencies {
// ... existing deps
implementation("com.google.firebase:firebase-messaging-ktx:24.0.0")
implementation("com.github.dmsyudha:gatiflow-android:v1.0.0")
}Also apply the Google Services plugin in the same file and ensure your project-levelbuild.gradle.kts has the plugin on the classpath:
// app/build.gradle.kts
plugins {
id("com.android.application")
id("com.google.gms.google-services") // add this
}
// project/build.gradle.kts
plugins {
id("com.google.gms.google-services") version "4.4.0" apply false
}Step 2 — Add google-services.json
Download google-services.json from Firebase Console → Project settings → Your app, and place it in app/ (same level as your app-levelbuild.gradle.kts).
Step 3 — Enable Push Notifications capability
Add the INTERNET permission to yourAndroidManifest.xml (usually auto-merged via the GatiFlow SDK manifest). Also declare your Firebase Messaging service:
<manifest>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<!-- ... -->
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>Step 4 — Create a Firebase Messaging service
Create a class that extends FirebaseMessagingService. When a new FCM token is issued (or refreshed), register it with GatiFlow:
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dev.gatiflow.sdk.GatiFlow
class MyFirebaseMessagingService : FirebaseMessagingService() {
/**
* Called when a new FCM token is issued (app install, token refresh, etc.).
* Register it with GatiFlow so campaigns can reach this device.
*/
override fun onNewToken(token: String) {
super.onNewToken(token)
GatiFlow.push?.registerToken(token)
}
/**
* Called when a push notification arrives while the app is in foreground.
* Handle it however you like — GatiFlow already delivered it.
*/
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
message.notification?.let { notif ->
// Show a local notification or update UI
}
}
}Step 5 — Register PushService
Add PushService::class.java to yourGatiFlow.start() services list:
import dev.gatiflow.sdk.GatiFlow
import dev.gatiflow.sdk.services.Crashes
import dev.gatiflow.sdk.services.Analytics
import dev.gatiflow.sdk.push.PushService
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
GatiFlow.start(
app = this,
appToken = "mhub_YOUR_APP_TOKEN",
services = listOf(
Crashes::class.java,
Analytics::class.java,
PushService::class.java, // add this
),
)
}
}Step 6 — Request notification permission (Android 13+)
Android 13 (API 33) requires a runtime permission for notifications. Request it at an appropriate moment, such as after a user action:
// In your Activity or Fragment (API 33+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
REQUEST_CODE_NOTIFICATIONS,
)
}
// After permission is granted, retrieve the current FCM token and register it
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {
GatiFlow.push?.registerToken(task.result)
}
}iOS SDK setup
The iOS integration uses native APNs. You request permission, pass the device token fromdidRegisterForRemoteNotificationsWithDeviceTokento GatiFlow, and that's it.
Step 1 — Enable Push Notifications capability
In Xcode, select your app target → Signing & Capabilities → + Capability → search for Push Notifications and add it. Also add Background Modes and enable Remote notifications.
Step 2 — Add PushService to your SDK initialization
In your SwiftUI App struct (orAppDelegate.application(_:didFinishLaunchingWithOptions:)):
import GatiFlow
@main
struct MyApp: App {
init() {
GatiFlow.start(
appToken: "mhub_YOUR_APP_TOKEN",
services: [
CrashService(),
AnalyticsService(),
PushService(), // add this
]
)
}
var body: some Scene {
WindowGroup { ContentView() }
}
}Step 3 — Request permission & register
Request notification permission early in your app flow (e.g. on first launch or after onboarding):
import UserNotifications
import UIKit
func requestPushPermission() {
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .badge, .sound]
) { granted, _ in
guard granted else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}Step 4 — Pass the APNs token to GatiFlow
In your AppDelegate (or viaUIApplicationDelegateAdaptor in SwiftUI):
import UIKit
import GatiFlow
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
// Pass the raw APNs token Data — GatiFlow converts it to a hex string
GatiFlow.push?.registerAPNsToken(deviceToken)
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("APNs registration failed: \(error)")
}
}
// SwiftUI — wire up AppDelegate
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// ...
}GatiFlow converts the raw Data token to a hex string automatically. Do not convert it yourself before passing it in.
Step 5 — Handle foreground notifications (optional)
import UserNotifications
// Set this early (e.g. in AppDelegate.didFinishLaunchingWithOptions)
UNUserNotificationCenter.current().delegate = self
extension AppDelegate: UNUserNotificationCenterDelegate {
// Show notification banner even when the app is in foreground
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler:
@escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .sound, .badge])
}
}Sending campaigns
Once your credentials are configured and at least one device has registered, you can send campaigns from the GatiFlow dashboard.
Creating a campaign
- Go to Push Notifications in the dashboard sidebar.
- Click New Campaign.
- Select the target app, fill in Title and Message.
- Optionally add a Data payload — arbitrary key-value pairs delivered to the app.
- Click Create to save as a draft, or Send now to deliver immediately.
Data payload
The optional data payload is delivered as a Map<String, String>on Android and a [String: String] dictionary in the APNsuserInfo on iOS. Use it to deep-link into a specific screen or pass context to the notification handler.
{
"screen": "product_detail",
"product_id": "prod_abc123",
"promo_code": "SUMMER20"
}Delivery report
After sending, the campaign status page shows:
| Field | Meaning |
|---|---|
| Sent count | Number of tokens FCM/APNs accepted |
| Failed count | Tokens rejected (device uninstalled, token rotated) |
| Sent at | Timestamp when delivery was initiated |
Failed tokens are automatically disabled in GatiFlow's device registry so they don't count against future sends. If a user reinstalls the app and registers again, the new token is stored.
API reference
SDK — device registration endpoint
The SDK communicates with the following endpoint. You don't call this directly unless you're building a custom integration.
POST /api/sdk/push/register
x-app-token: mhub_YOUR_APP_TOKEN
Content-Type: application/json
{
"token": "fcm-or-apns-device-token",
"platform": "android" | "ios"
}
// Response 200
{ "ok": true }
// Unregister
DELETE /api/sdk/push/register
x-app-token: mhub_YOUR_APP_TOKEN
Content-Type: application/json
{ "token": "fcm-or-apns-device-token" }Android — PushService API
| Method | Description |
|---|---|
| GatiFlow.push?.registerToken(token: String) | Register an FCM token with GatiFlow |
| GatiFlow.push?.unregisterToken(token: String) | Disable a token (e.g. on logout) |
iOS — PushService API
| Method | Description |
|---|---|
| GatiFlow.push?.registerAPNsToken(_ data: Data) | Convert raw APNs Data token and register it |
| GatiFlow.push?.registerToken(_ token: String, platform: String) | Register a pre-converted hex token |
| GatiFlow.push?.unregisterToken(_ token: String) | Disable a token (e.g. on logout) |
Troubleshooting
Push credentials not configured error when sending
Go to Apps → [Your App] → Settings and upload your Firebase service account JSON (for Android) or APNs .p8 key (for iOS). The Send button won't work until at least one credential is saved.
Notifications delivered in the dashboard but not received on device
Check that the device registered a token by looking at the device count shown on the campaign. On Android, confirm POST_NOTIFICATIONS permission was granted (required on API 33+). On iOS, confirm the Push Notifications capability is enabled and the correct Bundle ID matches the APNs key.
FCM service account validation fails
The JSON must be a Firebase service account file (type: "service_account") with project_id, client_email, and private_key. Do not use the Web API key from Firebase Console settings — that's a different kind of key.
APNs key validation fails
The .p8 file content must start with -----BEGIN PRIVATE KEY----- or -----BEGIN EC PRIVATE KEY-----. Make sure you're uploading the raw .p8 file, not a certificate or a certificate signing request.
Token registered but 0 devices shown on campaign
Token registration calls are fire-and-forget on the SDK side. Enable SDK debug logging and check that the registration network request returns 200. Also confirm the app token in GatiFlow.start() matches the app you're sending the campaign from.
GatiFlow.push is null on Android
PushService::class.java must be included in the services list passed to GatiFlow.start(). The push accessor returns null when the service is not registered.