Learn how to send notifications with FCM and APNs.
You may need finer-grained control over your notifications, so communicating directly with FCM and APNs may be necessary. The Expo platform does not lock you into using Expo's application services, and the expo-notifications
API is push-service agnostic.
Before communicating directly with FCM and APNs, there is one client-side change you'll need to make in your app. When using Expo's notification service, you collect the ExponentPushToken
with getExpoPushTokenAsync
. Now that you're not using Expo's notification service, you'll need to collect the native device token instead 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
After getting the native device token, you can start implementing the servers. Below are some minimal examples of communicating with FCM and APNs:
This documentation is based on Google's documentation, and this section covers the basics to get you started.
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 to configure FCM and get your FCM-SERVER-KEY
.
The following example uses FCM's legacy HTTP API, since the credentials setup for that is the same as it is for the Expo notifications service, so there's no additional work needed on your part. If you'd rather use FCM's HTTP v1 API, see Firebase official documentation.
await fetch('https://fcm.googleapis.com/fcm/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `key=<FCM-SERVER-KEY>`,
},
body: JSON.stringify({
to: '<NATIVE-DEVICE-PUSH-TOKEN>',
priority: 'normal',
data: {
experienceId: '@yourExpoUsername/yourProjectSlug',
scopeKey: '@yourExpoUsername/yourProjectSlug',
title: "📧 You've got mail",
message: 'Hello world! 🌐',
},
}),
});
The experienceId
and scopeKey
fields are required. 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.
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();
This example is minimal and includes no error handling and connection pooling. For testing purposes, you can refer to
sentNotificationToAPNS
code.
APNs provide their full list of supported fields in the notification payload.
The examples in the previous section provide the bare minimum notification requests. You may have to send category identifiers, custom sounds, icons, custom key-value pairs, and so on. expo-notifications
documents all the fields it supports, and below are the example payloads Expo sends in its notifications service:
{
"token": native device token string,
"collapse_key": string that identifies notification as collapsable,
"priority": "normal" || "high",
"data": {
"experienceId": "@yourExpoUsername/yourProjectSlug",
"scopeKey": "@yourExpoUsername/yourProjectSlug",
"title": title of your message,
"message": body of your message,
"channelId": the android channel ID associated with this notification,
"categoryId": the category associated with this notification,
"icon": the icon to show with this notification,
"link": the link this notification should open,
"sound": boolean or the custom sound file you'd like to play,
"vibrate": "true" | "false" | number[],
"priority": AndroidNotificationPriority, // https://docs.expo.dev/versions/latest/sdk/notifications/#androidnotificationpriority
"badge": the number to set the icon badge to,
"body": { object of key-value pairs }
}
}
{
"aps": {
"alert": {
"title": title of your message,
"subtitle": subtitle of your message (shown below title, above body),
"body": body of your message,
"launch-image": the name of the launch image file to display,
},
"category": the category associated with this notification,
"badge": number to set badge count to upon notification's arrival,
"sound": the sound to play when the notification is received,
"thread-id": app-specific identifier for grouping related notifications
},
"body": { object of key-value pairs },
"experienceId": "@yourExpoUsername/yourProjectSlug",
"scopeKey": "@yourExpoUsername/yourProjectSlug",
}
There are two types of Firebase Cloud Messaging messages: notification and data messages.
Notification messages are only handled (and displayed) by the Firebase library. They don't necessarily wake the app, and expo-notifications
will not be made aware that your app has received any notification.
Data messages are not handled by the Firebase library. They are immediately handed off to your app for processing. That's where expo-notifications
interprets the data payload and takes action based on that data. In almost all cases, this is the type of notification you have to send.
If you send a message of type notification instead of data directly through Firebase, you won't know if a user interacted with the notification (no onNotificationResponse
event available), and you won't be able to parse the notification payload for any data in your notification event-related listeners.
Using notification-type messages may have upsides when you need a configuration option that has not been exposed by
expo-notifications
yet. In general, it may lead to less predictable situations than using only data-type messages and it's not our responsibility that you'll have to go to Google to report issues.
Below is an example of each type using Node.js Firebase Admin SDK to send data-type messages instead of notification-type:
const devicePushToken = /* ... */;
const options = /* ... */;
// ❌ The following payload has a root-level notification object and
// it will not trigger expo-notifications and may not work as expected.
admin.messaging().sendToDevice(
devicePushToken,
{
notification: {
title: "This is a notification-type message",
body: "`expo-notifications` will never see this 😢",
},
data: {
photoId: 42,
},
},
options
);
// ✅ There is no "notification" key in the root level of the payload
// so the message is a "data" message, thus triggering expo-notifications.
admin.messaging().sendToDevice(
devicePushToken,
{
data: {
title: "This is a data-type message",
message: "`expo-notifications` events will be triggered 🤗",
// ⚠️ Notice the schema of this payload is different
// than that of Firebase SDK. What is there called "body"
// here is a "message". For more info see:
// https://docs.expo.dev/versions/latest/sdk/notifications/#android-push-notification-payload-specification
body: // As per Android payload format specified above, the
JSON.stringify({ photoId: 42 }), // additional "data" should be placed under "body" key.
},
},
options
);