Expo Recipe
Two ways to push an Expo app with Native Notify — the native-notify SDK on the Expo path, or the push API with the native APNs/FCM token from expo-notifications.
Expo apps have two supported routes:
- The Expo SDK path — start here. Install
native-notifyand callregisterNNPushToken(); the SDK registers Expo push tokens, and sends go through Expo's push service with receipts, inboxes and analytics. Follow Installation and Setup. - Universal push — register the native APNs/FCM token yourself. Choose this when you want sends to go directly through your own Firebase project and Apple key (no Expo push service in the middle).
Universal push in an Expo app
Get the native device token with expo-notifications and a stable device id with expo-application (npx expo install expo-notifications expo-application):
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 (an idempotent upsert — same deviceId, same device) and keep watchUniversalToken(...) subscribed while the app runs, so a rotated token is registered again. The app's push credentials are what make sends deliver.
expo-notifications displays these pushes on both platforms and hands your pushData to your listeners as notification.request.content.data — no extra rendering code is needed.
- Android: the build needs your Firebase config —
"android": { "googleServicesFile": "./google-services.json" }inapp.json— from the same Firebase project whose service-account JSON is saved in Native Notify (otherwise sends fail withSENDER_ID_MISMATCH). - iOS: an EAS/development build with push enabled; Native Notify sends with the app's
.p8key to its bundle id.
Expo Go:
getDevicePushTokenAsync() needs a development or EAS build. In Expo Go it throws on Android (SDK 53+), and on iOS the token it returns belongs to the Expo Go app — not yours — so your Apple key cannot deliver to it (DeviceTokenNotForTopic).
Which route should we choose?
| You want… | Route |
|---|---|
| The fastest Expo setup, Notification Inbox components, receipts, analytics app-opens | Expo SDK path — native-notify + registerNNPushToken() |
| Sends straight through your own Firebase/Apple credentials, one contract shared with Flutter/native apps | Universal push — this page |
| Both features in different apps | Both — the paths are independent per app |
Next steps
- Device Registration — every field and rule.
- Verify Delivery — test-send to the device you just registered.
- Send Notifications — blasts, device lists, subscriber lists.