HomeGuidesReferenceLearn
ArchiveExpo SnackDiscord and ForumsNewsletter

Send notifications with FCM & APNs

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.

How to write FCM and APNs servers

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:

FCM server

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.

How to find FCM server key

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).

APNs server

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.

Authorization

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:

  • APN key (.p8 file) associated with your app
  • Key ID of the above .p8 file
  • Your Apple Team ID
const 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",
    },
  }
);

HTTP/2 connection

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.

Payload formats

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:

Android

{
  "token": native device token string,
  "collapse_key": string that identifies notification as collapsible,
  "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 }
  }
}

iOS

{
  "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",
}

Firebase notification types

There are two types of Firebase Cloud Messaging messages: notification and data messages.

  1. 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.

  2. 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
);