PushNotice — Client Integration Guide

Everything a mobile developer needs to connect an app to the PushNotice platform.

  • Base URL: https://app.rspush.com
  • API root: https://app.rspush.com/api/v1
  • All requests and responses are JSON. HTTPS only.

1. What your app has to do

PushNotice delivers notifications through Firebase Cloud Messaging (FCM) — for both Android and iOS (iOS is relayed to APNs by Firebase). Your app:

  1. Integrates the Firebase Cloud Messaging SDK and obtains an FCM registration token.
  2. Sends that token to POST /subscribers (register).
  3. Re-sends it whenever Firebase rotates the token (refresh).
  4. Handles notification taps by reading the data payload (see §7).
  5. Optionally calls DELETE /subscribers when a user opts out.

PushNotice does not replace the Firebase SDK — you still initialise Firebase normally. PushNotice is the server that stores tokens, groups them, and sends to them.

2. Prerequisites

Requirement Notes
Firebase project Must be the same project whose service-account JSON is configured in PushNotice → App → Settings → Firebase credentials. Confirm the project_id matches.
google-services.json (Android) From that project, added to the Android app module.
GoogleService-Info.plist (iOS) From that project, added to the Xcode target. Exact filename, no suffix.
APNs key (iOS) A .p8 APNs Auth Key uploaded in Firebase Console → Project settings → Cloud Messaging → Apple app configuration. Without it, iOS pushes silently fail.
Firebase Messaging SDK firebase-messaging (Android) / FirebaseMessaging (iOS).
API key Generated in PushNotice → App → Settings → API keys. Format: pnp_ + 48 chars. Scoped to one app; can only register/unsubscribe devices — cannot send pushes or read data. Safe to embed in the app binary.

3. Authentication

Every request must present the app’s API key. Three accepted ways (pick one):

Authorization: Bearer pnp_xxxxxxxx…      ← preferred
X-Api-Key: pnp_xxxxxxxx…
?api_key=pnp_xxxxxxxx…                    ← query string, avoid

The {slug} in the URL path must match the app the key belongs to, or the request is rejected with 403.

Response Meaning
401 {"message":"Missing API key."} No key presented
401 {"message":"Invalid API key."} Key not recognised or revoked
403 {"message":"API key does not match this app."} Valid key, wrong app for {slug}

Revoking a key in the dashboard takes effect immediately.

4. Endpoints

POST /api/v1/apps/{slug}/subscribers — register / refresh

Idempotent on (app, token). Call on every app launch and on every token refresh — safe to call repeatedly.

Field Type Req. Notes
token string yes The FCM registration token. 10–4096 chars.
previous_token string no The device’s prior FCM token. On a refresh, pass the old token here so the existing record is migrated instead of a duplicate created.
platform string no ios | android | web | unknown. Defaults to unknown — send it.
external_id string no Your own user/account identifier, if the app has accounts. Max 255.
locale string no e.g. en-US. Max 20.
app_version string no e.g. 1.4.0. Max 50.
device_model string no e.g. iPhone15,3. Max 120.
meta object no Arbitrary JSON, stored verbatim.
groups string[] no Names of existing groups to add this device to. Unknown names are ignored. Every device is always added to the built-in Master group.
Status Body When
201 {"id":42,"status":"active","created":true} New device
200 {"id":42,"status":"active","created":false} Existing device refreshed
422 {"message":"…","errors":{"token":["…"]}} Validation failed

A device previously marked inactive or invalid is set back to active on a successful register.

curl -X POST https://app.rspush.com/api/v1/apps/ktso/subscribers \
  -H "Authorization: Bearer pnp_xxxxxxxx…" \
  -H "Content-Type: application/json" \
  -d '{
        "token": "fY3...:APA91b...",
        "platform": "ios",
        "app_version": "1.4.0",
        "locale": "en-US",
        "device_model": "iPhone15,3"
      }'

DELETE /api/v1/apps/{slug}/subscribers — unsubscribe

Soft unsubscribe: the record is kept for reporting, status set to inactive, and it stops receiving pushes. Call on logout / notifications-disabled.

Field Type Req.
token string yes

Response — always 200 {"ok":true}, even if the token was unknown.

curl -X DELETE https://app.rspush.com/api/v1/apps/ktso/subscribers \
  -H "Authorization: Bearer pnp_xxxxxxxx…" \
  -H "Content-Type: application/json" \
  -d '{"token": "fY3...:APA91b..."}'

5. Registration lifecycle

Event Action
App first launch (after notification permission granted) POST /subscribers with the current FCM token
Every subsequent launch POST /subscribers again (idempotent; refreshes last_seen, app_version, …)
Firebase onNewToken / didReceiveRegistrationToken POST /subscribers with token = new, previous_token = old
User disables notifications / logs out DELETE /subscribers with the current token
User re-enables POST /subscribers again — reactivates the record

Persist the last token you registered locally so you can supply previous_token on refresh.

6. Subscriber status & automatic cleanup

Status Meaning
active Receiving pushes
inactive Unsubscribed via DELETE — kept for history
invalid FCM reported the token as unregistered during a send. Set automatically; the device is skipped on future sends until it re-registers.

You don’t manage these — keep registering on launch/refresh and the platform keeps itself clean.

7. What the device receives

Each push is an FCM message with both a notification block (drives the OS notification) and a data block (key/value strings). Your tap handler should read from data.

notification

Key Source
title send title
body send body
image send image URL, if set

data (all values are strings)

Key Always? Source
title yes mirror of the notification title
body yes mirror of the notification body
send_id yes PushNotice’s ID for this send — use as the stable key for an on-device “clicked” archive
launch_url if set deep link / URL to open
url if set same value as launch_url (alias)
image_url if set image URL
(custom) — any key/value pairs added to the send’s Custom data field

Also set: Android priority high; WebPush fcm_options.link = launch_url when present.

Platform notes

Android — tap handling. When the app is backgrounded/killed, the OS shows the notification and, on tap, launches your activity with only the data payload in the intent extras — the notification title/body are not included. Read data.title / data.body / data.send_id from getIntent().getExtras(). This is why the data mirror exists; relying on the notification block leaves an on-device click archive blank on Android (older versions especially).
  • Android 13+ (API 33): request the POST_NOTIFICATIONS runtime permission.
  • Android notification channel: create a channel; without one, notifications are dropped silently on API 26+.
  • iOS — rich images: an image only renders if the app includes a Notification Service Extension that downloads the URL and attaches it. Firebase sets mutable-content automatically; the extension is your code. Text-only works without it.
  • iOS — tap handling: the full payload (including data) is delivered to userNotificationCenter(_:didReceive:), so both notification and data are readable.

8. Errors & retries

  • No rate limiting is enforced, but register once per launch, not in a loop.
  • All endpoints are idempotent — safe to retry.
  • Retry on network failure and 5xx with exponential backoff (e.g. 2s, 8s, 30s).
  • Do not retry 401 / 403 (fix the key) or 422 (fix the payload).

9. Groups

  • Groups (e.g. Test, Music Team) are created by operators in the PushNotice dashboard.
  • Every device is automatically in Master — the default target for a broadcast.
  • Your app may pass groups: ["Test"] at registration to self-assign to an existing group. Unknown names are ignored, not an error.
  • Operators can also move devices between groups from the dashboard.

10. Reference — minimal client snippets

iOS (Swift) — register on launch and on refresh

import FirebaseMessaging

func registerPushToken(_ token: String, previous: String? = nil) {
    var body: [String: Any] = ["token": token, "platform": "ios"]
    if let v = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
        body["app_version"] = v
    }
    if let previous { body["previous_token"] = previous }

    var req = URLRequest(url: URL(string: "https://app.rspush.com/api/v1/apps/ktso/subscribers")!)
    req.httpMethod = "POST"
    req.setValue("Bearer pnp_xxxxxxxx…", forHTTPHeaderField: "Authorization")
    req.setValue("application/json", forHTTPHeaderField: "Content-Type")
    req.httpBody = try? JSONSerialization.data(withJSONObject: body)
    URLSession.shared.dataTask(with: req).resume()
}

// MessagingDelegate
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
    guard let fcmToken else { return }
    let previous = UserDefaults.standard.string(forKey: "pnp_token")
    registerPushToken(fcmToken, previous: previous == fcmToken ? nil : previous)
    UserDefaults.standard.set(fcmToken, forKey: "pnp_token")
}

Android (Kotlin) — register, and read a tap

// FirebaseMessagingService
override fun onNewToken(token: String) {
    val previous = prefs.getString("pnp_token", null)
    registerPushToken(token, if (previous == token) null else previous)
    prefs.edit().putString("pnp_token", token).apply()
}

fun registerPushToken(token: String, previous: String?) {
    val json = JSONObject().apply {
        put("token", token)
        put("platform", "android")
        put("app_version", BuildConfig.VERSION_NAME)
        previous?.let { put("previous_token", it) }
    }
    val req = Request.Builder()
        .url("https://app.rspush.com/api/v1/apps/ktso/subscribers")
        .header("Authorization", "Bearer pnp_xxxxxxxx…")
        .post(json.toString().toRequestBody("application/json".toMediaType()))
        .build()
    okHttp.newCall(req).enqueue(/* retry on IOException / 5xx */)
}

// Launch Activity — record which push was tapped
val extras = intent.extras
val sendId = extras?.getString("send_id")
val title  = extras?.getString("title")
val body   = extras?.getString("body")
val url    = extras?.getString("launch_url")

11. Test checklist

  • Fresh install registers → device appears in the dashboard Subscribers tab as active, in Master.
  • Second launch → same subscriber row, last_seen updates, no duplicate.
  • Token refresh with previous_token → same row keeps its ID, token value changes.
  • Send a test push from the dashboard → arrives on iOS and Android.
  • Tap it on Android (app killed) → your click archive records send_id / title / body.
  • Tap it on iOS → same.
  • launch_url set → tapping opens the right screen/URL.
  • Uninstall the app, send again → next send marks that subscriber invalid automatically.
  • DELETE /subscribers → subscriber goes inactive, stops receiving.

Scroll to Top