TL;DR
- Remote push doesn't work in Expo Go on Android since SDK 53. Use a development build.
- Android needs FCM V1 credentials on EAS; iOS needs a paid Apple account and an APNs key.
- Create the Android notification channel before asking permission, or the prompt never shows on Android 13+.
- Pass
projectIdtogetExpoPushTokenAsyncexplicitly. - A ticket saying
okisn't delivery. Check receipts.
1. Install
npx expo install expo-notifications expo-constants 2. Credentials + dev build
| Platform | Needs |
|---|---|
| Android | FCM V1 credentials uploaded to EAS |
| iOS | Paid Apple Developer account + APNs key (EAS CLI offers to create it) |
eas build --profile development --platform android eas build --profile development --platform ios Register your iOS test device before the first build.
3. Permission + token
import * as Notifications from 'expo-notifications'; import Constants from 'expo-constants'; import { Platform } from 'react-native'; export async function registerForPushNotifications() { if (Platform.OS === 'android') { await Notifications.setNotificationChannelAsync('default', { name: 'default', importance: Notifications.AndroidImportance.MAX, }); } const { status: existing } = await Notifications.getPermissionsAsync(); let status = existing; if (existing !== 'granted') { ({ status } = await Notifications.requestPermissionsAsync()); } if (status !== 'granted') return null; const projectId = Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId; if (!projectId) throw new Error('Missing EAS projectId'); return (await Notifications.getExpoPushTokenAsync({ projectId })).data; } Save the token on your backend against the user. And add a handler, or foreground notifications stay hidden:
Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowBanner: true, shouldShowList: true, shouldPlaySound: false, shouldSetBadge: false, }), }); 4. Send one
Fastest: paste the token into expo.dev/notifications. From a server:
curl -X POST https://exp.host/--/api/v2/push/send \ -H "Content-Type: application/json" \ -d '{"to":"ExponentPushToken[xxx]","title":"Order shipped","body":"Tap to track","data":{"url":"myapp://orders/42"}}' You get a ticket back. Fetch the receipt later for real delivery errors like DeviceNotRegistered.
5. Handle taps
useEffect(() => { const sub = Notifications.addNotificationResponseReceivedListener((res) => { const url = res.notification.request.content.data?.url; if (typeof url === 'string') Linking.openURL(url); }); return () => sub.remove(); }, []); With Expo Router, myapp://orders/42 lands on app/orders/[id].tsx.
Didn't arrive? Check this
| Symptom | Cause |
|---|---|
| Nothing in Expo Go (Android) | No remote push there since SDK 53 |
| No permission prompt (Android) | Channel not created first |
| Token call throws | Missing projectId or credentials |
Ticket ok, nothing shows | Check receipt + device notification settings |
| Only fails in foreground | No setNotificationHandler |
DeviceNotRegistered | Stale token. Delete it. |
| Android fails, iOS fine | FCM V1 creds missing / wrong project |
Skipping the boilerplate
If you'd rather not hand-build the screens your notifications open, I work on RapidNative, which generates a standard Expo project (Expo Router, TypeScript, NativeWind) from a prompt. Export it and drop the code above in.
What broke your first Expo push: credentials, tokens, or the foreground handler?

