HomeGuidesReferenceLearn
ArchiveExpo SnackDiscord and ForumsNewsletter

Send notifications with Expo's Push API

Learn how to call Expo's Push API with the token when you want to send a notification.


The expo-notifications library provides all the client-side functionality for push notifications. Expo also handles sending these notifications off to FCM and APNs. All you need to do is send the request to the Expo's Push API with the ExpoPushToken you grabbed in the last step.

If you'd rather build a server that communicates with APNs and FCM directly, see Send notifications with FCM and APNs. It's more complicated than using Expo's push notification service.

Send push notifications using a server

After you setup your push notification credentials and add logic to get the ExpoPushToken, you can send it to the Expo API using an HTTPS POST request. You can do this by setting up a server with a database (or you can also write a command line tool to send them or send them straight from your app).

The Expo team and community have taken care of creating back-ends for you in a few different languages:

SDKsBack-endMaintained by
expo-server-sdk-nodeNode.jsExpo team
expo-server-sdk-pythonPythonCommunity
expo-server-sdk-rubyRubyCommunity
expo-push-notification-client-rustRustCommunity
expo-notifierSymfonySymfony
exponent-server-sdk-phpPHPCommunity
expo-server-sdk-phpPHPCommunity
exponent-server-sdk-golangGolangCommunity
exponent-server-sdk-elixirElixirCommunity
expo-server-sdk-dotnetdotnetCommunity
expo-server-sdk-javaJavaCommunity
laravel-expo-notifierLaravelCommunity

Each of the example servers above is a wrapper around Expo's Push API.

Implement push notifications reliably

Push Notifications travel through several systems from your server to recipient devices. Notifications are delivered most of the time. However, occasionally there are issues with systems along the way and the network connections between them. Handling outages and errors will help push notifications to arrive at their destinations more reliably.

Limit concurrent connections

When sending a large number of push notifications at once, limit the number of your concurrent connections. The Node SDK implements this for you and opens a maximum of six concurrent connections. This smooths out your peak load and helps the Expo push notification service receive push notifications successfully.

Retry on failure

The first step of sending push notifications is to deliver them to the Expo push notification service, which internally adds them to a queue for delivery to Google (FCM), Apple (APNs), or other push notification providers. This first step can fail for several reasons:

  • network issues between your server and the Expo push notification service
  • an outage or degraded availability of the Expo notification service
  • misconfigured push credentials
  • an invalid notification payload

Some of these failures are temporary. For example, if the Expo push notification service is down or unreachable and you get a network error, an HTTP 429 error (Too Many Requests), or an HTTP 5xx error (Server Errors), use exponential backoff to wait a few seconds before retrying. If the first retry attempt is unsuccessful, wait for longer (follow exponential backoff) and retry again. This lets the temporarily unavailable service recover before you retry.

Other failures will not resolve themselves. For example, if your push notification payload is malformed, you may get an HTTP 400 response explaining the issue with the payload. You will also get an error if there are no push credentials for your project or if you send push notifications for different projects in the same request.

Check push receipts for errors

The Expo push notification service responds with push tickets upon successfully receiving notifications. A push ticket indicates that Expo has received your notification payload but may still need to send it. Each push ticket contains a ticket ID, which you later use to look up a push receipt. A push receipt is available after Expo has tried to deliver the notification to FCM, APNs, and so on. It tells you whether delivery to the push notification provider was successful.

You must check your push receipts. If there is an issue delivering push notifications, the push receipts are the best way to get information about the underlying cause. For example, the receipts may indicate a problem with FCMs or APNs, the Expo push notification service, or your notification payload.

Push receipts may also tell you if a recipient device has unsubscribed from notifications (for example, by revoking notification permissions or uninstalling your app) if the push notification provider like APNs or FCM responds with that information. The push receipt will contain a details → error field set to DeviceNotRegistered. In this scenario, stop sending notifications to this device's push token until it re-registers with your server, so your app remains a good citizen. The DeviceNotRegistered error appears in push receipts only when Apple, Google, or another push notification provider deems the device to be unregistered. It takes an undefined amount of time and is often impossible to test by uninstalling your app and sending a push notification shortly after.

We recommend checking push receipts 15 minutes after sending your push notifications. While push receipts are often available much sooner, a 15-minute window gives the Expo push notification service a comfortable amount of time to make the receipts available to you. If after 15 minutes there is no push receipt, this likely indicates an error with the Expo push notification service. Lastly, push receipts are cleared after 24 hours.

SLAs

The Expo push notification service does not have an SLA and the FCM and APNs services also may have occasional outages. By following the guidance above, you can make your application robust against temporary service interruptions.

HTTP/2 API

Instead of using one of the libraries listed earlier, you may want to send requests directly to our HTTP/2 API (this API currently does not require any authentication).

To do so, send a POST request to https://exp.host/--/api/v2/push/send with the following HTTP headers:

host: exp.host
accept: application/json
accept-encoding: gzip, deflate
content-type: application/json

This is a "hello world" push notification using cURL that you can send using your CLI (replace the placeholder push token with your own):

curl -H "Content-Type: application/json" -X POST "https://exp.host/--/api/v2/push/send" -d '{
  "to": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
  "title":"hello",
  "body": "world"
}'

The request body must be JSON. It may either be a single message object (as shown in the example above) or an array of up to 100 message objects, as long as they are all for the same project as shown below. We recommend using an array when you want to send multiple messages to efficiently minimize the number of requests you need to make to Expo servers. Here's an example request body that sends four messages:

[
  {
    "to": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
    "sound": "default",
    "body": "Hello world!"
  },
  {
    "to": "ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]",
    "badge": 1,
    "body": "You've got mail"
  },
  {
    "to": [
      "ExponentPushToken[zzzzzzzzzzzzzzzzzzzzzz]",
      "ExponentPushToken[aaaaaaaaaaaaaaaaaaaaaa]"
    ],
    "body": "Breaking news!"
  }
]

The Expo server also optionally accepts gzip-compressed request bodies. This can greatly reduce the amount of upload bandwidth needed to send large numbers of notifications. The Node Expo Server SDK automatically gzips requests for you and automatically throttles your requests to smooth out the load, so we highly recommend it.

Push tickets

The requests above will respond with a JSON object with two optional fields, data and errors. data will contain an array of push tickets in the same order in which the messages were sent (or one push ticket object, if you send a single message to a single recipient). Each ticket includes a status field indicating whether Expo successfully received the notification and, if successful, an id field that can be used to retrieve a push receipt later.

A status of ok along with a receipt ID means that the message was received by Expo's servers, not that it was received by the user (for that you will need to check the push receipt).

Continuing the above example, this is what a successful response body looks like:

{
  "data": [
    { "status": "ok", "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" },
    { "status": "ok", "id": "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY" },
    { "status": "ok", "id": "ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ" },
    { "status": "ok", "id": "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA" }
  ]
}

If there were errors with individual messages, but not the entire request, the bad messages' corresponding push tickets will have a status of error, and fields that describe the error as shown below:

{
  "data": [
    {
      "status": "error",
      "message": "\"ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]\" is not a registered push notification recipient",
      "details": {
        "error": "DeviceNotRegistered"
      }
    },
    {
      "status": "ok",
      "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
    }
  ]
}

If the entire request failed, the HTTP status code will be 4xx or 5xx and the errors field will be an array of error objects (usually just one). Otherwise, the HTTP status code will be 200 and your messages will be on their way to the Android and iOS push notification services.

Push receipts

After receiving a batch of notifications, Expo enqueues each notification to deliver to the Android and iOS push notification services (FCM and APNs, respectively). Most notifications are typically delivered within a few seconds. However, sometimes it may take longer to deliver notifications, particularly if the Android or iOS push notification services take longer than usual to receive and deliver notifications or if Expo's cloud infrastructure is under high load.

Once Expo delivers a notification to the Android or iOS push notification service, Expo creates a push receipt that indicates whether the Android or iOS push notification service successfully received the notification. If there was an error in delivering the notification, perhaps due to faulty credentials or service downtime, the push receipt will contain more information regarding that error.

To fetch the push receipts, send a POST request to https://exp.host/--/api/v2/push/getReceipts. The request body must be a JSON object with a field name ids that is an array of ticket ID strings:

curl -H "Content-Type: application/json" -X POST "https://exp.host/--/api/v2/push/getReceipts" -d '{
  "ids": [
    "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY",
    "ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ"
  ]
}'

The response body for push receipts is very similar to that of push tickets; it is a JSON object with two optional fields, data and errors. data contains a mapping of receipt IDs to receipts. Receipts include a status field, and two optional message and details fields (in the case where "status": "error"). If there is no push receipt for a requested receipt ID, the mapping won't contain that ID. This is what a successful response to the above request looks like:

{
  "data": {
    "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX": { "status": "ok" },
    "ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ": { "status": "ok" }
    // When there is no receipt with a given ID (YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY in this
    // example), the ID is omitted from the response.
  }
}

You must check each push receipt, since they may contain information about errors you need to resolve. For example, if a device is no longer eligible to receive notifications, Apple's documentation asks that you stop sending notifications to that device. The push receipts will contain information about these errors.

Even if a receipt's status says ok, this doesn't guarantee that the device has received the message; "ok" in a push receipt means that the Android or iOS push notification service successfully received the notification. If the recipient device is turned off, for example, the iOS or Android push notification service will try to deliver the message but the device won't necessarily receive it.

If the entire request failed, the HTTP status code will be 4xx or 5xx and the errors field will be an array of error objects (usually just one). Otherwise, the HTTP status code will be 200 and your messages will be on their way to your users' devices.

Errors

Expo provides details regarding any errors that occur during this entire process. We'll cover some of the most common errors below so that you can implement logic to handle them automatically on your server.

If for whatever reason, Expo couldn't deliver the message to the Android or iOS push notification service, the push receipt's details may also include service-specific information. This is useful mostly for debugging and reporting possible bugs to Expo.

Individual errors

Inside both push tickets and push receipts, look for a details object with an error field. If present, it may be one of the following values, and you should handle these errors like so:

Push ticket errors

  • DeviceNotRegistered: The device cannot receive push notifications anymore and you should stop sending messages to the corresponding Expo push token.

Push receipt errors

  • DeviceNotRegistered: The device cannot receive push notifications anymore and you should stop sending messages to the corresponding Expo push token.

  • MessageTooBig: The total notification payload was too large. On Android and iOS, the total payload must be at most 4096 bytes.

  • MessageRateExceeded: You are sending messages too frequently to the given device. Implement exponential backoff and slowly retry sending messages.

  • MismatchSenderId: This indicates that there is an issue with your FCM push credentials. There are two pieces to FCM push credentials: your FCM server key, and your google-services.json file. Both must be associated with the same sender ID. You can find your sender ID in the same place you find your server key. Check that the server key from your project's Expo dashboard under Credentials > Application Identifier > FCM Server Key and that the sender ID from your project's google-services.json > project_number is the same as shown in the Firebase console under Project Settings > Cloud Messaging tab > Cloud Messaging API (Legacy).

  • InvalidCredentials: Your push notification credentials for your standalone app are invalid (for example, you may have revoked them).

    • Android: Make sure that you have correctly uploaded the server key from the Firebase Console as specified in uploading FCM V1 server credentials.
    • iOS: Run eas credentials and follow the prompts to regenerate new push notification credentials. If you revoke an APN key, all apps that rely on that key will no longer be able to send or receive push notifications until you upload a new key to replace it. Uploading a new APN key will not change your users' Expo Push Tokens. Sometimes, these errors will contain further details claiming an InvalidProviderToken error. This is actually tied to both your APN key and your provisioning profile. To resolve this error, you should rebuild the app and regenerate a new push key and provisioning profile.

For a better understanding of iOS credentials, including push notification credentials, read our App Signing docs.

Request errors

If there's an error with the entire request for either push tickets or push receipts, the errors object might have one of the following values, and you should handle these errors:

  • TOO_MANY_REQUESTS: You are exceeding the request limit of 600 notifications per second per project. We recommend implementing rate-limiting in your server to prevent sending more than 600 notifications per second (note that if you use expo-server-sdk-node, this is already implemented along with exponential backoffs for retries).

  • PUSH_TOO_MANY_EXPERIENCE_IDS: You are trying to send push notifications to different Expo experiences, for example, @username/projectAAA and @username/projectBBB. Check the details field for a mapping of experience names to their associated push tokens from the request, and remove any from another experience.

  • PUSH_TOO_MANY_NOTIFICATIONS: You are trying to send more than 100 push notifications in one request. Make sure you are only sending 100 (or fewer) notifications in each request.

  • PUSH_TOO_MANY_RECEIPTS: You are trying to get more than 1000 push receipts in one request. Make sure you are only sending an array of 1000 (or fewer) ticket ID strings to get your push receipts.

Additional security

You can require any push requests to be sent with a valid access token before we will deliver them to your users. You can enable this enhanced push security from your Expo Dashboard.

By default, you can send a notification to your users by sending their Expo Push Token and any text or additional data needed for the message. This is easy to set up, but if the tokens are leaked, a malicious user would be able to impersonate your app and send their message to your users. We have never had an instance of this report. However, to follow best security practices, we offer the use of an access token alongside the push token as an additional layer of security.

If you're using the expo-server-sdk-node, upgrade to at least v3.6.0 and pass your accessToken as an option in the constructor. Otherwise, pass in the header 'Authorization': 'Bearer ${accessToken}' with any requests to our push API.

Any requests sent without a valid access token after you enable push security will result in an error with code: UNAUTHORIZED.

Formats

Message request format

Each message must be a JSON object with the given fields (only the to field is required):

FieldPlatformTypeDescription
toAndroid and iOSstring | string[]An Expo push token or an array of Expo push tokens specifying the recipient(s) of this message.
dataAndroid and iOSObjectA JSON object delivered to your app. It may be up to about 4KiB; the total notification payload sent to Apple and Google must be at most 4KiB or else you will get a "Message Too Big" error.
titleAndroid and iOSstringThe title to display in the notification. Often displayed above the notification body
bodyAndroid and iOSstringThe message to display in the notification.
ttlAndroid and iOSnumberTime to Live: the number of seconds for which the message may be kept around for redelivery if it hasn't been delivered yet. Defaults to undefined to use the respective defaults of each provider (0 for iOS/APNs and 2419200 (4 weeks) for Android/FCM).
expirationAndroid and iOSnumberTimestamp since the Unix epoch specifying when the message expires. Same effect as ttl (ttl takes precedence over expiration).
priorityAndroid and iOS'default' | 'normal' | 'high'The delivery priority of the message. Specify "default" or omit this field to use the default priority on each platform ("normal" on Android and "high" on iOS).
subtitleiOS OnlystringThe subtitle to display in the notification below the title.
soundiOS Only'default' | nullPlay a sound when the recipient receives this notification. Specify "default" to play the device's default notification sound, or omit this field to play no sound. Custom sounds are not supported.
badgeiOS OnlynumberNumber to display in the badge on the app icon. Specify zero to clear the badge.
channelIdAndroid OnlystringID of the Notification Channel through which to display this notification. If an ID is specified but the corresponding channel does not exist on the device (that has not yet been created by your app), the notification will not be displayed to the user.
categoryIdAndroid and iOSstringID of the notification category that this notification is associated with. Find out more about notification categories here. Must be on at least SDK 41 or bare workflow.
mutableContentiOS OnlybooleanSpecifies whether this notification can be intercepted by the client app. In Expo Go, this defaults to true, and if you change that to false, you may experience issues. In standalone and bare apps, this defaults to false.

Note on ttl: On Android, we make our best effort to deliver messages with zero TTL immediately and do not throttle them. However, setting TTL to a low value (for example, zero) can prevent normal-priority notifications from ever reaching Android devices that are in doze mode. To guarantee that a notification will be delivered, TTL must be long enough for the device to wake from doze mode. This field takes precedence over expiration when both are specified.

Note on priority: On Android, normal-priority messages won't open network connections on sleeping devices and their delivery may be delayed to conserve the battery. High-priority messages are delivered immediately if possible and may wake sleeping devices to open network connections, consuming energy. On iOS, normal-priority messages are sent at a time that takes into account power considerations for the device and may be grouped and delivered in bursts. They are throttled and may not be delivered by Apple. High-priority messages are sent immediately. Normal priority corresponds to APNs priority level 5 and high priority to 10.

Note on channelId: If left null, a "Default" channel will be used, and Expo will create the channel on the device if it does not yet exist. However, use caution, as the "Default" channel is user-facing and you may not be able to fully delete it.

Push ticket format

{
  "data": [
    {
      "status": "error" | "ok",
      "id": string, // this is the Receipt ID
      // if status === "error"
      "message": string,
      "details": JSON
    },
    ...
  ],
  // only populated if there was an error with the entire request
  "errors": [{
    "code": string,
    "message": string
  }]
}

Push receipt request format

{
  "ids": string[]
}

Push receipt response format

{
  "data": {
    Receipt ID: {
      "status": "error" | "ok",
      // if status === "error"
      "message": string,
      "details": JSON
    },
    ...
  },
  // only populated if there was an error with the entire request
  "errors": [{
    "code": string,
    "message": string
  }]
}

Delivery guarantees

Expo makes the best effort to deliver notifications to the push notification services operated by Google and Apple. Expo's infrastructure is designed for at least once delivery to the underlying push notification services. It is more likely for a notification to be delivered to Google or Apple more than once rather than not at all, though both are uncommon and possible.

After a notification has been handed off to an underlying push notification service, Expo creates a "push receipt" that records whether the handoff was successful. A push receipt denotes whether the underlying push notification service received the notification.

Finally, the push notification services from Google, Apple, and so on follow their policies to deliver the notifications to the device.