expo-server-sdk-node: Push Notifications Tickets status always "ok"

I am trying to handle the push notifications errors in my backend, in order to avoid sending them to users which hasn’t granted the respective permission.

These are the steps I am following:

  1. Go to the iOS settings > Expo and disable push notifications permission
  2. Send a push notification (connecting to my endpoint) to the user who has disabled the permission.

The docs says that App Stores will remove apps which doesn’t handle the error “DeviceNotRegistered”…

Will the user device be registered if yesterday he granted the permissions but today he has disallowed them?

Here is the code I am using (similar to the provided on GitHub)

async function handleErrorsSendingMessages(tickets) {
      Sending a notification to APNs or FCM via the Expo Server produces a ticket, which
      contains a receipt ID we have to use in order to get the receipt for that notification.
      Note: The receipts may contain error codes to which we must respond. In
      particular, Apple or Google may block apps that continue to send
      notifications to devices that have blocked notifications or have uninstalled
      your app. Expo does not control this policy and sends back the feedback from
      Apple and Google so we can handle it appropriately.
    const receiptIds = [];
    for (const ticket of tickets) {
        NOTE: Not all tickets have IDs; for example, tickets for notifications
        that could not be enqueued will have error information and not a receipt ID.
      if (ticket.id) {

    // Retrieve batches of receipts from the Expo service in parallel.
    const receiptIdChunks = expo.chunkPushNotificationReceiptIds(receiptIds);

    const promises = receiptIdChunks.map(
      (receiptIdChunk) =>
        new Promise((resolve, reject) => {
            .then((receipts) => resolve(receipts))
            .catch((err) => reject(err));

    const receipts = await Promise.allSettled(promises)
      .then((results) => {
        const receipts = [];
        results.forEach((result) => {
          if (result.status === "rejected") {
            console.error(`Error retrieving the recipts: ${result.reason}`);
          } else {

        return receipts;
      .catch((err) => {

      The receipts specify whether Apple or Google were correctly notified
      and information about an error, if it occurred.
    for (const receiptId in receipts) {
      const { status, message, details } = receipts[receiptId];

      if (status === "ok") {

      if (status === "error") {
        console.error(`There was an error sending a notification: ${message}`);

        if (details && details.error) {
            The error codes are listed in the Expo documentation:

          console.error(`The error code is ${details.error}`);

          // TODO - Handle errors in a future if necessary

As I am storing the tokens in my database, I have thought to do these steps when the tickets have errors:

Not implemented yet

(In the backend)

  1. Delete the token from the database
  2. Only send a push notification (talking about my endpoints’ code) when it was possible to retrieve the token from the DB

(In the frontend)

  1. Listen notifications permission (using AppState)
  2. If the previous notifications permission was “granted” but now is not “granted”, then call an endpoint which will delete the user token from the DB.

Is this a good way to handle this? Why I am getting correct responses (tickets) when sending push notifications to devices that have cancelled the notifications permission?

Also, is there a way to get the push notification token which caused the error? It seems that the tickets information only contains the receipt id, and the error details.

Thank you.

The push tickets you receive in the response from sending push notifications are in the same order as the messages you sent, so you can map them to the relevant expoPushToken :grinning_face_with_smiling_eyes:

Currently, the DeviceNotRegistered error seems to be quite rare to receive on iOS. This looks like it’s actually an upstream APNs issue, not an issue with Expo though. On Android, it seems to be working as expected

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.