Platform guides

Expo recipe

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:

app.json
{
  "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

universalPush.js
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.