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 projectId to getExpoPushTokenAsync explicitly.
  • A ticket saying ok isn'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?