skip to content

In an Expo app, when do you use Notifications.getExpoPushTokenAsync() versus getDevicePushTokenAsync(), and what does each return?

level: middleimportance: should knowfreq 45%

answer

  1. who sends: Expo's service or your server
  2. ExponentPushToken[...] versus native token
  3. projectId from app config
  4. Expo re-registers rotated device tokens
  5. network call versus local call

basics

~10 s

getExpoPushTokenAsync() returns an ExponentPushToken for sending through Expo's push service, which relays to APNs or FCM; getDevicePushTokenAsync() returns the native APNs or FCM token for servers that talk to those providers directly.

solid answer

~40 s

Choose by **who sends the push**. If your backend uses **Expo's push service**, call `Notifications.getExpoPushTokenAsync({ projectId })`: it gets the native token, registers it with Expo's servers and resolves to `{ type: 'expo', data: 'ExponentPushToken[...]' }`. It needs the EAS `projectId` (read from `Constants.expoConfig?.extra?.eas?.projectId` or `Constants.easConfig?.projectId`) and a network round trip, so wrap it in `try/catch` and retry later. If your backend talks to **APNs and FCM directly**, call `Notifications.getDevicePushTokenAsync()`, which resolves locally to `{ type: 'ios' | 'android', data }` with the provider's own token. A useful difference: after you fetch an Expo token, `expo-notifications` automatically re-registers a rotated native token with Expo, so the Expo token stays the same; a device token you store yourself must be kept current with `addPushTokenListener`.

code

typescript · 21 lines
typescript
import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';

export async function getExpoToken(): Promise<string | null> {
  const projectId =
    Constants.expoConfig?.extra?.eas?.projectId ?? Constants.easConfig?.projectId;
  if (!projectId) {
    throw new Error('EAS projectId is missing from app config');
  }
  try {
    const token = await Notifications.getExpoPushTokenAsync({ projectId });
    return token.data; // 'ExponentPushToken[...]'
  } catch {
    return null; // offline or Expo unreachable: retry on next launch
  }
}

export async function getNativeToken() {
  const token = await Notifications.getDevicePushTokenAsync();
  return { platform: token.type, token: token.data }; // APNs on iOS, FCM on Android
}

go deeper

for a junior

Recall that the Expo token is for Expo's push service and the device token is for sending through APNs or FCM directly.

for a middle

Explain what each call returns, the projectId and network requirements of the Expo token, and how auto-registration keeps it valid when the native token rotates.

for a senior

Choose by backend architecture, handle offline token fetches and custom endpoints that disable auto-registration, and store tokens per installation with their provider type.

for a principal

Decide whether routing pushes through Expo's relay is acceptable for the product's reliability and data requirements, or whether to own APNs and FCM integration.

## Two tokens, two senders `expo-notifications` can give the app two different push addresses. They are not interchangeable, and the choice depends on the backend: | | `getExpoPushTokenAsync()` | `getDevicePushTokenAsync()` | |---|---|---| | Resolves to | `{ type: 'expo', data: 'ExponentPushToken[...]' }` | `{ type: 'ios', data }` (APNs) or `{ type: 'android', data }` (FCM) | | Who sends pushes | your server calls **Expo's push service**, which relays to APNs or FCM | your server calls **APNs and FCM** directly | | Network call from the app | yes, to Expo's servers | no, returns the provider's token | | Needs | the EAS `projectId`, push credentials uploaded to EAS | provider credentials on your own server | | When the native token rotates | the library re-registers it with Expo; the Expo token stays | you must detect it and update your server | ## getExpoPushTokenAsync in detail 1. It first obtains the native device token (the same one `getDevicePushTokenAsync()` returns). 2. It sends that token, the app's `applicationId`, an installation ID, the `projectId` and whether the build uses the development APNs environment to Expo's servers. 3. Expo returns the `ExponentPushToken[...]` string for that installation and project. 4. The library then turns on **automatic server registration**: a global native-token listener re-sends any rotated device token to Expo, and a persisted flag retries the upload on the next launch if it failed. Practical consequences: - **`projectId` is required.** If it cannot be inferred, typically in a bare project, the call throws an error saying no `projectId` was found. Expo's docs recommend passing it explicitly from `expo-constants`. - **It can fail for network reasons.** The docs tell you to `try/catch` it and retry once the device is online; an offline first launch should not break onboarding. - **Custom `url` or `baseUrl` options turn off auto-registration.** If you point the call at your own endpoint, keeping the token current becomes your job again. - The Expo token is **stable across app upgrades**, may change on an Android reinstall, and changes if the `applicationId` changes. It does not expire; after an uninstall, sends to it eventually return `DeviceNotRegistered`. ## getDevicePushTokenAsync in detail - It returns the provider's own token without contacting Expo. - On Android it needs Firebase configured (`android.googleServicesFile` in app config), because the token is an FCM token. - Use it when the backend already sends through FCM and APNs, for example because it also serves a web or native app, or when you do not want a relay in the path. - Pair it with `Notifications.addPushTokenListener(listener)`, which fires with a new `{ type, data }` when the native token changes and returns a subscription you `remove()` on cleanup. ## Choosing for the pharmacy app - A small team with an Expo backend integration and no existing push stack: **Expo push tokens**. One API for both platforms, credentials managed through EAS, rotation handled. - A backend that already sends FCM messages to Android and APNs to iOS for other clients: **device tokens**, plus a refresh listener and per-install storage. Either way, the app sends the token to your backend tied to the signed-in user, and the backend removes tokens that the sending service reports as unregistered. ## Error handling in practice Both calls are asynchronous and can reject, so treat token acquisition as best effort: 1. Run it after sign-in, not during the first render of the app. 2. On failure, keep the app usable and schedule a retry on the next launch or when the app returns to the foreground. 3. Log the failure reason without logging the token itself. 4. Only mark the device as registered once your own backend has acknowledged the token. This matters most for the Expo token, whose network dependency means a first launch on a weak connection is the most common failure. ## Common mistakes - Storing the `data` of an Expo token and sending it to FCM, or storing an APNs token and sending it to Expo's API; each token only works with its own service. - Forgetting `projectId` in a bare or custom-built app. - Calling `getExpoPushTokenAsync` on every render instead of once per launch or sign-in.

  • If you use Expo push tokens, do you still need addPushTokenListener?
    Usually not for delivery: after `getExpoPushTokenAsync` succeeds, `expo-notifications` enables automatic registration, so a rotated native token is re-sent to Expo and the `ExponentPushToken` stays valid. You still re-send the Expo token to your own backend on sign-in, and the listener becomes necessary if you pass a custom `url` or `baseUrl`, which disables that auto-registration.
  • Why can getExpoPushTokenAsync fail on a first launch when getDevicePushTokenAsync works?
    The Expo token requires a request to Expo's servers, so it fails when the device is offline, the request times out, or the `projectId` is missing. The device token is returned by the provider without contacting Expo. Wrap the Expo call in `try/catch` and retry later rather than blocking onboarding on it.

saying these in an interview costs you the question

  • An ExponentPushToken can be sent to FCM or APNs directly
  • getExpoPushTokenAsync works offline because the token is generated locally
  • projectId is optional in every project
  • The Expo push token changes every time the native token rotates
  • getDevicePushTokenAsync returns an Expo token on Android