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:
- 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.
- 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.
- Upload both with the app's iOS bundle id (the APNs topic) and Android package.
- Make a server push key (
aps_…, shown once) and keep it in your server's settings. - 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"}
]}| Field | Meaning |
|---|---|
platform | ios or android (required) |
token | The device token (required). iOS tokens are hex. |
title, body | The text shown. Leave both empty for a silent background push. |
data | Your own keys, passed to the app (on Android every value arrives as a string). |
sound | default or a sound file bundled in the app. |
badge | The app icon's number (iOS), notification count (Android). |
collapse_id | A newer message with the same id replaces the older one. |
thread_id | Groups notifications on iOS. |
priority | high (default) or normal. |
sandbox | true 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 appDelegateBuilds 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>.