aipps.dev

Push notifications

Keep your APNs key and Firebase service account on aipps.dev once, then your server sends to iPhones and Android phones with one call and one key. Your server keeps the device tokens; aipps.dev passes each message to Apple or Google and tells you which tokens are no longer valid.

Setup

Open the app in the console and go to Push notifications. The checklist walks through it:

  1. Apple push key: developer.apple.com → Certificates, Identifiers & Profiles → Keys → + → tick Apple Push Notifications service (APNs) → Register → Download the .p8 (only once). Note the Key ID and your Team ID. Turn on Push Notifications for the app's identifier.
  2. Firebase project: console.firebase.google.com → add a project and an Android app with your package name → Project settings → Service accounts → Generate new private key.
  3. Upload both with the app's iOS bundle id (the APNs topic) and Android package.
  4. Make a server push key (aps_…, shown once) and keep it in your server's settings.
  5. Send a test to a device token from your app.

The same from the command line, in the project folder:

aipps credentials set apns AuthKey_ABC123.p8 --team-id TEAM123456
aipps credentials set fcm my-project-firebase-adminsdk.json
aipps push key create my-server
aipps push test -p ios --token <hex token> --sandbox
aipps push                     # what's in place, today's counts, last error

Send API

POST https://aipps.dev/api/push/send
Authorization: Bearer aps_...
Content-Type: application/json

{"messages": [
  {"platform": "ios", "token": "a1b2c3...", "title": "New message", "body": "Ana: see you at 6",
   "data": {"chat": "42"}, "sound": "default", "badge": 3, "thread_id": "chat-42"},
  {"platform": "android", "token": "fcm-token...", "title": "New message", "body": "Ana: see you at 6",
   "data": {"chat": "42"}, "collapse_id": "chat-42"}
]}
FieldMeaning
platformios or android (required)
tokenThe device token (required). iOS tokens are hex.
title, bodyThe text shown. Leave both empty for a silent background push.
dataYour own keys, passed to the app (on Android every value arrives as a string).
sounddefault or a sound file bundled in the app.
badgeThe app icon's number (iOS), notification count (Android).
collapse_idA newer message with the same id replaces the older one.
thread_idGroups notifications on iOS.
priorityhigh (default) or normal.
sandboxtrue for iOS debug builds from Xcode (APNs sandbox). Store, TestFlight and ad hoc builds use the default, production.

Up to 500 messages a call. Messages go out in parallel.

Results

{"ok": 1, "results": [
  {"status": "ok", "reason": ""},
  {"status": "unregistered", "reason": "UNREGISTERED"}
]}

One result per message, in order: ok; unregistered (the app was removed or the token expired: delete the token on your side); invalid (a malformed token or payload); error(keys missing or rejected, or the provider failed: try again later). reason is Apple's or Google's own code, such as BadDeviceToken or InvalidProviderToken.

curl, Python, Node

curl -s https://aipps.dev/api/push/send \
  -H "Authorization: Bearer $AIPPS_PUSH_KEY" -H "Content-Type: application/json" \
  -d '{"messages": [{"platform": "android", "token": "'$TOKEN'", "title": "Hi", "body": "It works"}]}'
# Python (httpx or requests)
import os, httpx

def push(messages):
    r = httpx.post("https://aipps.dev/api/push/send", timeout=30,
                   headers={"Authorization": f"Bearer {os.environ['AIPPS_PUSH_KEY']}"},
                   json={"messages": messages})
    r.raise_for_status()
    return r.json()["results"]

tokens = [("ios", "a1b2c3..."), ("android", "fcm-token...")]
results = push([{"platform": p, "token": t, "title": "Order shipped", "body": "Arrives Friday"} for p, t in tokens])
for (p, t), res in zip(tokens, results):
    if res["status"] == "unregistered":
        forget_token(t)        # your own code
// Node 18+ (built-in fetch)
async function push(messages) {
  const res = await fetch("https://aipps.dev/api/push/send", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.AIPPS_PUSH_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ messages }),
  });
  if (!res.ok) throw new Error(`push: ${res.status} ${await res.text()}`);
  return (await res.json()).results;
}

await push([{ platform: "ios", token, title: "Order shipped", body: "Arrives Friday", sandbox: false }]);

Device tokens

The app asks the user for permission, gets a token from Apple or Firebase, and sends it to your server with the platform. Tokens change now and then: send the latest one each time the app starts.

iOS (Swift)

Turn on the Push Notifications capability in Xcode (and Background Modes → Remote notifications for silent pushes).

import UIKit
import UserNotifications

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ app: UIApplication,
                     didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
            if granted { DispatchQueue.main.async { app.registerForRemoteNotifications() } }
        }
        return true
    }

    func application(_ app: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let token = deviceToken.map { String(format: "%02x", $0) }.joined()   // hex, what aipps.dev expects
        MyServer.saveToken(platform: "ios", token: token)                      // your own code
    }
}
// SwiftUI: @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

Builds run from Xcode are debug builds: send to them with "sandbox": true.

Android (Kotlin)

Add Firebase Messaging (com.google.firebase:firebase-messaging) and google-services.json to the app module. On Android 13 and newer, ask for POST_NOTIFICATIONS.

// Activity
if (Build.VERSION.SDK_INT >= 33) {
    registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
        .launch(Manifest.permission.POST_NOTIFICATIONS)
}
FirebaseMessaging.getInstance().token.addOnSuccessListener { token ->
    MyServer.saveToken(platform = "android", token = token)   // your own code
}

// New tokens later
class PushService : FirebaseMessagingService() {
    override fun onNewToken(token: String) = MyServer.saveToken("android", token)
}
// AndroidManifest.xml: <service android:name=".PushService" android:exported="false">
//   <intent-filter><action android:name="com.google.firebase.MESSAGING_EVENT" /></intent-filter></service>

No google-services.json in your repo? Store it once with aipps credentials set google-services google-services.json and aipps builds add it.

React Native

Use @react-native-firebase/messaging (FCM tokens on Android, and with getAPNSToken() the raw APNs token on iOS) or react-native-notifications (native tokens on both). Expo's push service and its ExponentPushToken tokens don't go through aipps.dev: send the device's own token instead (with Expo, getDevicePushTokenAsync() from expo-notifications).

import messaging from "@react-native-firebase/messaging";
import { Platform } from "react-native";

export async function registerForPush() {
  const status = await messaging().requestPermission();     // iOS prompt; Android 13+ also needs POST_NOTIFICATIONS
  if (status === messaging.AuthorizationStatus.DENIED) return;
  await messaging().registerDeviceForRemoteMessages();
  const token = Platform.OS === "ios" ? await messaging().getAPNSToken() : await messaging().getToken();
  if (token) await saveToken(Platform.OS, token);           // your own code
  messaging().onTokenRefresh((t) => Platform.OS === "android" && saveToken("android", t));
}

Limits and privacy

Each app can send 10,000 notifications a day (UTC); a call that would pass it is refused whole with HTTP 429. aipps.dev stores no device tokens and no message text, only each day's sent and failed counts and the last error, which the console shows. The APNs key and service account are encrypted and used only to send; build machines never get them. Revoke a push key in the console or with aipps push key revoke <id>.