Register an Expo app's native APNs and FCM tokens with Native Notify, or use the native-notify SDK on Expo's push service.
Two routes
Sends here go through your own Firebase project and Apple key. For Expo's push service — the native-notify SDK with inbox components, receipts and analytics — follow Expo setup. The routes are independent per app.
1. Set up the project
npx expo install expo-notifications expo-application
Android needs your Firebase config, from the same Firebase project whose service-account JSON you save in step 4 — or sends fail with SENDER_ID_MISMATCH:
{
"expo": {
"android": { "googleServicesFile": "./google-services.json" }
}
}
Expo Go
Use a development or EAS build, with push enabled on iOS. In Expo Go, getDevicePushTokenAsync() throws on Android (SDK 53+), and on iOS it returns Expo Go's token, not yours — your Apple key can't deliver to it (DeviceTokenNotForTopic).
2. Register the device
import * as Notifications from "expo-notifications";
import * as Application from "expo-application";
import { Platform } from "react-native";
const REGISTER_URL = "https://app.nativenotify.com/api/universal/device/register";
// Stable across launches: the vendor id on iOS, ANDROID_ID on Android.
async function stableDeviceId() {
const id = Platform.OS === "ios" ? await Application.getIosIdForVendorAsync() : Application.getAndroidId();
if (!id) throw new Error("No device id is available yet — retry on the next launch.");
return id;
}
let lastRegisteredToken = null;
async function postRegistration({ appId, appToken, subscriberId }, devicePushToken) {
const token = String(devicePushToken.data);
if (token === lastRegisteredToken) return null; // already registered this session
const res = await fetch(REGISTER_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
appId,
appToken,
deviceId: await stableDeviceId(),
platform: Platform.OS,
subscriberId, // your logged-in user's id; omit for an anonymous device
tokens: Platform.OS === "ios" ? { apnsToken: token } : { fcmToken: token },
}),
});
if (res.status !== 201) {
// 400/403 answer { error: { code, message } }; a wrong appId/appToken answers 401 as plain text.
throw new Error(`Native Notify register failed (${res.status}): ${await res.text()}`);
}
lastRegisteredToken = token;
return res.json(); // { ok, device, tokens }
}
export async function registerUniversalDevice(args) {
if (Platform.OS !== "ios" && Platform.OS !== "android") return null;
const { status } = await Notifications.requestPermissionsAsync();
if (status !== "granted") return null;
// The NATIVE token: APNs on iOS, FCM on Android (development/EAS builds only).
const devicePushToken = await Notifications.getDevicePushTokenAsync();
return postRegistration(args, devicePushToken);
}
// Re-register when the native token rotates. Use the token the listener hands
// you: calling getDevicePushTokenAsync() inside this listener re-fires it.
export function watchUniversalToken(args) {
const subscription = Notifications.addPushTokenListener((devicePushToken) => {
postRegistration(args, devicePushToken).catch((err) => console.warn("Native Notify:", err));
});
return () => subscription.remove();
}
Call registerUniversalDevice({ appId, appToken, subscriberId }) on every launch, and keep watchUniversalToken(...) subscribed so a rotated token registers again.
3. Show pushes
expo-notifications displays pushes on both platforms and hands your pushData to listeners as notification.request.content.data.
4. Save credentials
Sends deliver only with the app's push credentials: the Firebase service-account JSON (Android) and the .p8 key for the app's bundle id (iOS).
5. Send a push
Check one device first with a test send, then send to an audience.