File: sending-notifications-custom.md | Updated: 11/15/2025
Hide navigation
Search
Ctrl K
Home Guides EAS Reference Learn
Archive Expo Snack Discord and Forums Newsletter
Copy page
Learn how to send notifications with FCM and APNs.
Copy page
You may need finer-grained control over your notifications, in which case communicating directly with FCM and APNs may be necessary. The Expo platform does not lock you into using Expo Application Services, and the expo-notifications API is push-service agnostic.
Note: This guide does not aim to be a comprehensive resource for sending notifications via FCM or APNs. We recommend you read the official documentation to make sure you're following the latest instructions.
Obtaining a device token for FCM or APNs
When using Expo notification service, you use the ExpoPushToken obtained with getExpoPushTokenAsync
.
If you instead want to send notifications via FCM or APNs, you need to obtain the native device token with getDevicePushTokenAsync
.
import * as Notifications from 'expo-notifications'; ... - const token = (await Notifications.getExpoPushTokenAsync()).data; + const token = (await Notifications.getDevicePushTokenAsync()).data; // send token to your server
This guide is based on Firebase official documentation .
Communicating with FCM is done by sending a POST request. However, before sending or receiving any notifications, you'll need to follow the steps to configure FCM
and get your FCM-SERVER-KEY.
FCM requires an Oauth 2.0 access token, which must be obtained via one of the methods described in "Update authorization of send requests" .
For testing purposes, you can use the Google Auth Library and your private key file obtained above, to obtain a short lived token for a single notification, as in this Node example adapted from Firebase documentation:
import { JWT } from 'google-auth-library'; function getAccessTokenAsync( key: string // Contents of your FCM private key file ) { return new Promise(function (resolve, reject) { const jwtClient = new JWT( key.client_email, null, key.private_key, ['https://www.googleapis.com/auth/cloud-platform'], null ); jwtClient.authorize(function (err, tokens) { if (err) { reject(err); return; } resolve(tokens.access_token); }); }); }
Show More
The example code below calls getAccessTokenAsync() above to get the Oauth 2.0 token, then constructs and sends the notification POST request. Note that unlike FCM legacy protocol, the endpoint for the request includes the name of your Firebase project.
// FCM_SERVER_KEY: Environment variable with the path to your FCM private key file // FCM_PROJECT_NAME: Your Firebase project name // FCM_DEVICE_TOKEN: The client's device token (see above in this document) async function sendFCMv1Notification() { const key = require(process.env.FCM_SERVER_KEY); const firebaseAccessToken = await getAccessTokenAsync(key); const deviceToken = process.env.FCM_DEVICE_TOKEN; const messageBody = { message: { token: deviceToken, data: { channelId: 'default', message: 'Testing', title: `This is an FCM notification message`, body: JSON.stringify({ title: 'bodyTitle', body: 'bodyBody' }), scopeKey: '@yourExpoUsername/yourProjectSlug', experienceId: '@yourExpoUsername/yourProjectSlug', }, }, }; const response = await fetch( `https://fcm.googleapis.com/v1/projects/${process.env.FCM_PROJECT_NAME}/messages:send`, { method: 'POST', headers: { Authorization: `Bearer ${firebaseAccessToken}`, Accept: 'application/json', 'Accept-encoding': 'gzip, deflate', 'Content-Type': 'application/json', }, body: JSON.stringify(messageBody), } ); const readResponse = (response: Response) => response.json(); const json = await readResponse(response); console.log(`Response JSON: ${JSON.stringify(json, null, 2)}`); }
Show More
The experienceId and scopeKey fields are only applicable when using Expo Go (from SDK 53, push notifications support is removed from Expo Go). Otherwise, your notifications will not go through to your app. FCM has a list of supported fields in the notification payload
, and you can see which ones are supported by expo-notifications on Android by looking at the FirebaseRemoteMessage
.
FCM also provides some server-side libraries in a few different languages
you can use instead of raw fetch requests.
Your FCM server key can be found by making sure you've followed the configuration steps
, and instead of uploading your FCM key to Expo, you would use that key directly in your server (as the FCM-SERVER-KEY in the previous example).
This documentation is based on Apple's documentation , and this section covers the basics to get you started.
Communicating with APNs is a little more complicated than with FCM. Some libraries wrap all of this functionality into one or two function calls such as node-apn
. However, in the examples below, a minimum set of libraries are used.
Receiving push notifications only works if your iOS app has the APNs entitlement. For apps using CNG , the Expo config needs to be modified in one of two ways:
expo-notifications library to your app, and ensure that its plugin appears in the plugins array of your app config
:app.json
Copy
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "plugins": [ %%placeholder-start%%... %%placeholder-end%% "expo-notifications" ] } }
expo-notifications library, then you should add the aps-environment entitlement manually to the Expo configuration
, as in the example below:app.json
Copy
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "ios": { %%placeholder-start%%... %%placeholder-end%% "entitlements": { "aps-environment": "development" } } } }
If you are not using CNG, then you should add the push notification entitlement in Xcode .
Note: If you are upgrading an Expo app from a version from SDK 51 and earlier, you should refer to this FYI document .
Initially, before sending requests to APNS, you need permission to send notifications to your app. This is granted via a JSON web token which is generated using iOS developer credentials:
.p8 file) associated with your app.p8 fileconst jwt = require("jsonwebtoken"); const authorizationToken = jwt.sign( { iss: "YOUR-APPLE-TEAM-ID" iat: Math.round(new Date().getTime() / 1000), }, fs.readFileSync("./path/to/appName_apns_key.p8", "utf8"), { header: { alg: "ES256", kid: "YOUR-P8-KEY-ID", }, } );
After getting the authorizationToken, you can open up an HTTP/2 connection to Apple's servers. In development, send requests to api.sandbox.push.apple.com. In production, send requests to api.push.apple.com.
Here's how to construct the request:
const http2 = require('http2'); const client = http2.connect( IS_PRODUCTION ? 'https://api.push.apple.com' : 'https://api.sandbox.push.apple.com' ); const request = client.request({ ':method': 'POST', ':scheme': 'https', 'apns-topic': 'YOUR-BUNDLE-IDENTIFIER', ':path': '/3/device/' + nativeDeviceToken, // This is the native device token you grabbed client-side authorization: `bearer ${authorizationToken}`, // This is the JSON web token generated in the "Authorization" step }); request.setEncoding('utf8'); request.write( JSON.stringify({ aps: { alert: { title: "📧 You've got mail!", body: 'Hello world! 🌐', }, }, experienceId: '@yourExpoUsername/yourProjectSlug', // Required when testing in the Expo Go app scopeKey: '@yourExpoUsername/yourProjectSlug', // Required when testing in the Expo Go app }) ); request.end();
Show More
This example is minimal and includes no error handling and connection pooling. For testing purposes, you can refer to
sendNotificationToAPNSexample code.
APNs provide their full list of supported fields in the notification payload .