HomeGuidesReferenceLearn
ArchiveExpo SnackDiscord and ForumsNewsletter

Send notifications with FCMv1 & APNs

Edit this page

Learn how to send notifications with FCMv1 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.

For documentation on communicating with the legacy FCM service, see Send notifications with FCM legacy server.
Before sending a notification directly through FCMv1 or APNs, you will need to obtain a device token.

FCMv1 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 FCMv1 and get your FCM-SERVER-KEY.

Getting an authentication token

FCMv1 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);
    });
  });
}

Sending the notification

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)}`);
}

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 example 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 can be beneficial when you need a configuration option that is not yet exposed by expo-notifications. Generally, it may lead to less predictable situations than using data-type messages. However, you may need to report any issue you encounter directly to Google.

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