Developers · API v1.0.0
Expo Integration
This guide is for apps built with Expo (managed workflow or development builds). For the general API reference, see the Overview.
Supported versions: Expo SDK 50+ (recommended: latest SDK) · iOS 13.4+ · Android 7.0+ (API 24)
Remote push notifications don't work in Expo Go (support was removed on Android as of SDK 53). For testing, use a development build or a build produced with EAS Build. Push notifications must be tested on a physical device, not an emulator/simulator.
1. Installation
npx expo install expo-notifications expo-device expo-constants expo-secure-store expo-localization expo-crypto
app.json / app.config.js:
{
"expo": {
"plugins": [
["expo-notifications", { "defaultChannel": "default" }]
],
"extra": {
"eas": { "projectId": "YOUR-EAS-PROJECT-ID" }
}
}
}
Upload your push credentials to your Expo project (one time):
eas credentials # Android: FCM V1 service account JSON, iOS: APNs key
2. NotiPilot client
src/notipilot.ts:
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import * as SecureStore from 'expo-secure-store';
import * as Localization from 'expo-localization';
import * as Crypto from 'expo-crypto';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
const NOTIPILOT_BASE_URL = 'https://app.notipilot.com/api/v1';
// Must match the "Expo Project ID" in the NotiPilot dashboard
const PROJECT_ID: string =
Constants.expoConfig?.extra?.eas?.projectId ?? Constants.easConfig?.projectId;
const DEVICE_UID_KEY = 'notipilot_device_uid';
// On SDK 52 and earlier, use shouldShowAlert: true instead of shouldShowBanner/shouldShowList
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
async function getDeviceUid(): Promise<string> {
let uid = await SecureStore.getItemAsync(DEVICE_UID_KEY);
if (!uid) {
uid = Crypto.randomUUID();
await SecureStore.setItemAsync(DEVICE_UID_KEY, uid);
}
return uid;
}
async function post(path: string, body: object, attempt = 0): Promise<any> {
const res = await fetch(`${NOTIPILOT_BASE_URL}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json().catch(() => ({}));
if ((res.status === 429 || res.status >= 500) && attempt < 3) {
const waitSec = Number(json.retry_after) || 2 ** attempt;
await new Promise((r) => setTimeout(r, waitSec * 1000));
return post(path, body, attempt + 1);
}
if (!res.ok) {
console.warn('[NotiPilot]', res.status, json.error, json.errors ?? json.message);
}
return json;
}
async function getExpoPushToken(): Promise<string | null> {
if (!Device.isDevice) return null;
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'General',
importance: Notifications.AndroidImportance.HIGH,
});
}
let { status } = await Notifications.getPermissionsAsync();
if (status !== 'granted') {
({ status } = await Notifications.requestPermissionsAsync());
}
if (status !== 'granted') return null;
const { data } = await Notifications.getExpoPushTokenAsync({ projectId: PROJECT_ID });
return data; // "ExponentPushToken[...]"
}
export type NotiPilotAttributes = Record<string, string | number | boolean | null>;
/** Call on every app launch. */
export async function registerDevice(extra: NotiPilotAttributes = {}) {
const token = await getExpoPushToken();
if (!token) return null;
const locale = Localization.getLocales()[0];
return post('/register-device', {
app_id: PROJECT_ID,
device_uid: await getDeviceUid(),
token,
platform: Platform.OS, // 'ios' | 'android'
provider: 'expo',
attributes: {
locale: locale?.languageCode ?? null, // "tr"
country: locale?.regionCode ?? null, // "TR"
app_version: Constants.expoConfig?.version ?? null,
...extra, // e.g. { city: 'Istanbul' }
},
});
}
/** Call when the user signs in. */
export async function identify(externalId: string, attributes?: NotiPilotAttributes) {
return post('/identify-device', {
app_id: PROJECT_ID,
device_uid: await getDeviceUid(),
external_id: externalId,
...(attributes ? { attributes } : {}),
});
}
/** Call when the user signs out. */
export async function logout() {
const token = await getExpoPushToken();
if (!token) return null;
return post('/register-device', {
app_id: PROJECT_ID,
device_uid: await getDeviceUid(),
token,
platform: Platform.OS,
external_id: null,
});
}
/** Keeps NotiPilot up to date when the token is refreshed. Call once at app startup. */
export function listenForTokenChanges() {
return Notifications.addPushTokenListener(() => {
registerDevice().catch(() => {});
});
}
3. Usage
import { useEffect } from 'react';
import * as Notifications from 'expo-notifications';
import { registerDevice, listenForTokenChanges, identify } from './src/notipilot';
export default function App() {
useEffect(() => {
registerDevice({ city: 'Istanbul' }).catch(console.warn);
const tokenSub = listenForTokenChanges();
// When a notification is tapped, read the `data` field sent from the dashboard
const tapSub = Notifications.addNotificationResponseReceivedListener((response) => {
const data = response.notification.request.content.data;
// e.g. data.screen === 'product' → navigation.navigate('Product', { id: data.product_id })
});
return () => {
tokenSub.remove();
tapSub.remove();
};
}, []);
// After sign-in: await identify(user.id, { gender: user.gender });
return null;
}
4. Checklist
- Expo Project ID in the dashboard =
extra.eas.projectId - FCM V1 and APNs credentials uploaded with
eas credentials - Tested on a physical device with a development/production build
-
countryand, if possible,cityare being sent (for location segments) -
identifyis called after sign-in andlogouton sign-out
Common issues
| Symptom | Solution |
|---|---|
404 unknown_app |
The app_id is not registered in the dashboard or is incorrect. Check the Expo Project ID in the dashboard. |
| Can't get a token | Use a physical device, check notification permission, and make sure you pass the projectId parameter. |
| Registration succeeds but no notifications arrive | The FCM V1 / APNs credentials may be missing from the Expo project. Send a test notification to the token with the Expo push tool. |
| Notifications arrive silently on Android | Create a notification channel with the ID default and HIGH importance. |