---
source_url: https://www.pubnub.com/docs/general/push/android
title: Android Mobile Push Notifications
updated_at: 2026-05-22T11:05:16.158Z
---

> Documentation Index
> For a curated overview of PubNub documentation, see: https://www.pubnub.com/docs/llms.txt
> For the full list of all documentation pages, see: https://www.pubnub.com/docs/llms-full.txt


# Android Mobile Push Notifications

PubNub Mobile Push Notifications bridges native message publishing with third‑party push services, including Apple Push Notification service (APNs) and Firebase Cloud Messaging (FCM). The examples below show how to register for remote notifications, receive the device token, register it on a PubNub channel, and publish a message.

Before you use Mobile Push Notifications with Firebase Cloud Messaging (FCM), configure both your Firebase and PubNub accounts for your app.

## Step 1: Configure account settings

Before you use PubNub Mobile Push Notifications with Firebase Cloud Messaging (FCM), configure both your Firebase and PubNub accounts for your app.

### Step 1a: Download a Firebase private key file

:::warning Update your keyset configuration
If you use the [legacy HTTP FCM](https://firebase.google.com/docs/cloud-messaging/http-server-ref) to send Mobile Push Notifications, you must migrate to the new [HTTP v1 FCM](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages) before June 2024. Refer to the [migration guide](https://www.pubnub.com/docs/general/resources/migration-guides/legacy-fcm-migration-guide) for details.
:::

Log into the [Firebase console](https://console.firebase.google.com). Click **Create a project** and follow the wizard.

![Firebase Create Project](https://www.pubnub.com/assets/images/firebase-create-project-bf2a8b856a0ed02c2e1303368b5777d8.png)

Click the settings icon at the top left of your project overview page. Go to **Project settings**.

![Firebase Project Settings](https://www.pubnub.com/assets/images/firebase-project-settings-48653e64d56b6c3af9e3118a43e6d34e.png)

Switch to the **Service accounts** tab.

![Firebase Generate New Private Key](https://www.pubnub.com/assets/images/firebase-generate-new-private-key-9498bd5c25f399efd66143993076d25a.png)

The default service account has nearly admin privileges for a customer’s entire Firebase project. PubNub requires access to only Firebase messaging to send notifications via FCM.

We recommend that you create a dedicated service account with minimal permissions ([Firebase Cloud Messaging API Admin](https://cloud.google.com/iam/docs/roles-permissions/firebasecloudmessaging) role) to limit access to only what’s needed for PubNub’s FCM use.

To create a service account, click the external link under **All service accounts** to open Google Cloud Platform.

Fill in the service account details. Select the Firebase Cloud Messaging API Admin role from the dropdown and click **Done**.

Back in the Firebase console, open the new service account. Click **Generate new private key** and download your FCM private key file.

### Step 1b: Upload the Firebase private key file to your Admin Portal

On the [Admin Portal](https://admin.pubnub.com), go to the Mobile Push Notifications section on your app's keyset. Enable the feature and upload the file downloaded from Firebase through the **Private key file** option in the Firebase Cloud Messaging section.

![Admin Portal Upload Private Keyset File](https://www.pubnub.com/assets/images/portal-upload-private-key-file-842e5a4eeea8f1b4b394f5f67c8f0cc0.png)

:::note Migrate from legacy FCM APIs to HTTP v1
The Legacy FCM APIs that used an API Key have been [deprecated by Google](https://firebase.google.com/docs/cloud-messaging/migrate-v1), and will be removed in June 2024. The Admin Portal will continue supporting the legacy FCM API Keys until Google removes those deprecated APIs entirely. Still, it is strongly recommended that you configure FCM HTTP v1 following the steps described in this section.
:::

## Step 2: Request device token

Open your Android Studio project and switch to the `Project` view. Add the `google-services.json` file (from Step 1) to your app module root directory.

![Set FCM API Key](https://www.pubnub.com/assets/images/firebase_android_add_json_config-d502e8c6cedea9ec273bd969804ea5ed.png)

### Step 2a: Configure Gradle

Refer to the [Google documentation](https://firebase.google.com/docs/android/setup#add-config-file) for steps to configure your Gradle files to use the `google-services` plugin and add Firebase dependencies.

### Step 2b: Firebase registration

The following example shows how to get a token from [FirebaseMessaging](https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/FirebaseMessaging). You can do this anywhere in the app. This example uses `onCreate` of [AppCompatActivity](https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity) so it always runs.

Read [Firebase Docs](https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/FirebaseMessaging#public-taskstring-gettoken) for more details.

#### Java

```java
public class MainActivity extends AppCompatActivity {
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    SharedPreferencesManager.init(getApplicationContext())

    FirebaseMessaging.getInstance().getToken().addOnCompleteListener(new OnCompleteListener<String>() {
      @Override public void onComplete(@NonNull Task<String> task) {
        if (!task.isSuccessful()) {
          Log.w(TAG, "Fetching FCM registration token failed", task.getException());
          return;
        }
        Log.d(TAG, getString(R.string.msg_token_fmt, task.getResult()));
      }
    });
  }
}
```

#### Kotlin

```kotlin
class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    Firebase.messaging.getToken().addOnCompleteListener(OnCompleteListener { task ->
      if (!task.isSuccessful) {
        Log.w(TAG, "Fetching FCM registration token failed", task.exception)
        return@OnCompleteListener
      }

      Log.d(TAG, getString(R.string.msg_token_fmt, task.result))
    })
  }
}
```

## Step 3: Receive & monitor device token

To receive the device token (and updates) and mobile push notifications, create a class that extends [FirebaseMessagingService](https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/FirebaseMessagingService).

The `onNewToken` callback fires whenever FCM generates a new token. After initial registration, FCM may deliver new tokens. Update your cached token and PubNub channel registrations when the token changes.

### Java

```java
public class MyFirebaseMessagingService extends FirebaseMessagingService {
  @Override  public void onNewToken(String token) {
    String oldToken = SharedPreferencesManager.readDeviceToken();
    if (token.equals(oldToken)) { return; }
    SharedPreferencesManager.write(token);

    updatePushNotificationsOnChannels(SharedPreferencesManager.readChannelList(), token, oldToken);
  }
}
```

### Kotlin

```kotlin
override fun onNewToken(token: String) {
  val oldToken = SharedPreferencesManager.getInstance(applicationContext).readDeviceToken()
  if (token == oldToken) { return  }
  SharedPreferencesManager.getInstance(applicationContext).writeDeviceToken(token)

  updatePushNotificationsOnChannels(
    SharedPreferencesManager.getInstance(applicationContext).readChannelList(),
    token,
    oldToken
  )
}
```

### Step 3a: Cache device token & registered channels

To ensure that an application can properly handle adding, updating, and removing registered push channels, there are two pieces of information that should be cached to avoid race-conditions: the Device Token and the list of registered channels. Not only will properly caching allow for easy access from anywhere inside the application, it will also prevent against race-conditions when multiple registration operations are queued at the same time.

[SharedPreferences](https://developer.android.com/training/data-storage/shared-preferences) provides basic persistent storage, but can be replaced by more sophisticated storage as your use-case requires. The following code will ensure the accessing of the cached information will be thread-safe regardless of storage choice.

A new Device Token can be provided from the system at anytime, and should be stored whenever it's received. The list of registered channels should also be cached in a similar manner as the Device Token, and should be updated whenever registered channels are added or removed. [Set](https://developer.android.com/reference/java/util/Set) is used for the convenience of ensuring there are no duplicate channels.

#### Java

```java
public class SharedPreferencesManager {
  private static SharedPreferences sharedPref;
  private SharedPreferencesManager() { }

  public static void init(Context context) {
    if(sharedPref == null)
      sharedPref = context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
  }

  public static final String FCM_DEVICE_TOKEN = "PUBNUB_FCM_DEVICE_TOKEN";
  public static @Nullable String readDeviceToken() {
    return sharedPref.getString(FCM_DEVICE_TOKEN, null)
  }
  public static void writeDeviceToken(String value) {
    SharedPreferences.Editor prefsEditor = sharedPref.edit();
    prefsEditor.putString(FCM_DEVICE_TOKEN, value);
    prefsEditor.apply();
  }

  public static final String FCM_CHANNEL_LIST = "PUBNUB_FCM_CHANNEL_LIST";
  public static @Nullable String[] readChannelList() {
    return (String[]) sharedPref.getStringSet(FCM_CHANNEL_LIST, null).toArray();
  }
  public static void writeChannelList(Set<String>  value) {
    SharedPreferences.Editor prefsEditor = sharedPref.edit();
    prefsEditor.putStringSet(FCM_CHANNEL_LIST, value);
    prefsEditor.apply();
  }
}
```

#### Kotlin

```kotlin
class SharedPreferencesManager private constructor()  {
  companion object {
    private val sharePref = SharedPreferencesManager()
    private lateinit var sharedPreferences: SharedPreferences

    private val PLACE_OBJ = "place_obj"
    private val FCM_DEVICE_TOKEN = "PUBNUB_FCM_DEVICE_TOKEN"
    private val FCM_CHANNEL_LIST = "PUBNUB_FCM_CHANNEL_LIST"

    fun getInstance(context: Context): SharedPreferencesManager {
      if (!::sharedPreferences.isInitialized) {
        synchronized(SharedPreferencesManager::class.java) {
          if (!::sharedPreferences.isInitialized) {
            sharedPreferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
          }
        }
      }
      return sharePref
    }
  }

  fun readDeviceToken(): String? = sharedPreferences.getString(FCM_DEVICE_TOKEN, null)
  fun writeDeviceToken(value: String?) = sharedPreferences.edit().putString(FCM_DEVICE_TOKEN, value).apply()

  fun readChannelList(): List<String> = sharedPreferences.getStringSet(FCM_CHANNEL_LIST, null)?.toList().orEmpty()
  fun writeChannelList(value: Set<String>?) = sharedPreferences.edit().putStringSet(FCM_CHANNEL_LIST, value).apply()
}
```

### Step 3b: Update existing registrations

A simple helper method can be created to consolidate the remove-then-add functionality when updating your existing registered channels.

#### Java

```java
public void updatePushNotificationsOnChannels(String[] channels, String deviceToken, String oldToken) {
  if (oldToken != null) {
    pubnub.removeAllPushNotificationsFromDeviceWithPushToken()
      .pushType(PNPushType.FCM)
      .deviceId(oldToken)
      .async(result -> { /* check result */ });
  }

  pubnub.addPushNotificationsOnChannels()
    .pushType(PNPushType.FCM)
    .deviceId(deviceToken)
    .channels(channels)
    .async(result -> { /* check result */ });
}
```

#### Kotlin

```kotlin
fun updatePushNotificationsOnChannels(channels: List<String>, deviceToken: String, oldToken: String?) {
  oldToken?.also { token ->
    pubnub.removeAllPushNotificationsFromDeviceWithPushToken(
      pushType = PNPushType.FCM,
      deviceId = token
    ).async { result, status -> }
  }

  pubnub.addPushNotificationsOnChannels(
    pushType = PNPushType.FCM,
    deviceId = deviceToken,
    channels = channels
  ).async { result, status ->
    // Handle Response
  }
}
```

## Step 4: Manage device registrations

After you obtain a device token, register it with a list of channels to allow Mobile Push Notifications to be sent to the device. You can add or remove channels at any time. You can also view current registrations for a device token.

### Step 4a: Register new channels

When adding channels, it's recommended to obtain the Device Token and List of Registered Channels from a [cached source](#step-3a-cache-device-token--registered-channels). After successfully registering channels, the newly registered channels should be added to the cached list.

#### Java

```java
String cachedToken = SharedPreferencesManager.readDeviceToken();

pubnub.addPushNotificationsOnChannels()
  .pushType(PNPushType.FCM)
  .deviceId(cachedToken)
  .channels(Arrays.asList("ch1", "ch2", "ch3"))
  .async(result -> { /* check result */ });
```

#### Kotlin

```kotlin
SharedPreferencesManager.getInstance(applicationContext).readDeviceToken()?.also { deviceToken ->
  pubnub.addPushNotificationsOnChannels(
    pushType = PNPushType.FCM,
    deviceId = deviceToken,
    channels = listOf("ch1", "ch2", "ch3")
  ).async { result, status ->
    // Handle Response
  }
}
```

### Step 4b: List registered channels

Once device registrations are added, you can confirm the APNs registrations for the device by listing all channels that the device is registered with. Since the list on the server is the source-of-truth, we will update our [cached](#step-3a-cache-device-token--registered-channels) list to reflect the channels currently registered on the server.

#### Java

```java
String cachedToken = SharedPreferencesManager.readDeviceToken();

pubnub.auditPushChannelProvisions()
  .pushType(PNPushType.FCM)
  .deviceId(cachedToken)
  .async(result -> { /* check result */ });
```

#### Kotlin

```kotlin
SharedPreferencesManager.getInstance(applicationContext).readDeviceToken()?.also { deviceToken ->
  pubnub.auditPushChannelProvisions(
    pushType = PNPushType.FCM,
    deviceId = deviceToken
  ).async { result, status ->
    // Handle Response
  }
}
```

### Step 4c: Remove existing registrations

When removing channels it's recommended to obtain the Device Token and List of Registered Channels from a [cached source](#step-3a-cache-device-token--registered-channels). After removing registering channels, the channels that were removed should be also removed from the cached source.

#### Java

```java
String cachedToken = SharedPreferencesManager.readDeviceToken();

pubnub.removePushNotificationsFromChannels()
  .pushType(PNPushType.FCM)
  .deviceId(cachedToken)
  .channels(Arrays.asList("ch1", "ch2", "ch3"))
  .async(result -> { /* check result */ });
```

#### Kotlin

```kotlin
SharedPreferencesManager.getInstance(applicationContext).readDeviceToken()?.also { deviceToken ->
  pubnub.removePushNotificationsFromChannels(
    pushType = PNPushType.FCM,
    deviceId = deviceToken,
    channels = listOf("ch1", "ch2", "ch3")
  ).async { result, status ->
    // Handle Response
  }
}
```

## Step 5: Construct the push payload

To send a push notification, include the appropriate push notification payload for FCM when you publish a message and PubNub will appropriately parse the message.

#### FCM mobile push notification payload structure

The structure of the `pn_fcm` payload is as follows:

```json
{
  "pn_fcm": {
    "notification": {
      "title": "My Title",
      "body": "message notification text"
    },
    "android": {
      "collapse_key": "group",
      "data": {
        "age": "10"
      },
      "ttl": "30s"
    },
    "webpush": {
      // FCM Webpush config
    },
    "apns": {
      // FCM APNS config
    },
    "fcm_options": {
      // FCM Options
    },
    "pn_exceptions": [
      "optional-excluded-device-token1"
    ]
  },
  "pn_debug": true,
  "pn_dry_run": false
}
```

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `pn_fcm` | `Object` | Yes | Container for Firebase Cloud Messaging (FCM) payload. The specification is compatible with [FCM message payload structure](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages) to ensure PubNub processes the message correctly when delivering it to FCM. |
| `> notification` | `Object` | No | Basic notification template to use across all platforms. This part is platform-agnostic, but within the context of `pn_fcm`, it's being prepared for FCM parsing. It can take several optional parameters, like `title`, `body`, or `image`. Check official [Firebase docs](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#notification) for the full list of available parameters. |
| `> android` | `Object` | No | Android-specific options for messages sent through FCM connection server. Specifies how messages should be handled on Android devices. It can take several optional parameters, like `collapse_key`, `data`, or `ttl`. Check official [Firebase docs](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#AndroidConfig) for the full list of available parameters. |
| `> pn_exceptions` | [String] | No | A list of Device Tokens that should be excluded from receiving the notification. |
| `pn_debug` | boolean | No | A flag that enables push debugging info to the `pndebug` channel. For more information, refer to [Mobile Push Troubleshooting](https://www.pubnub.com/docs/general/push/mobile-push-troubleshooting#debug-messages). |
| `pn_dry_run` | boolean | No | A flag that allows developers to test a request without actually sending a message. |

:::warning Don't specify topic, token, and condition
**Don't** specify the `token`, `topic`, or `condition` [targets](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages) in the payload on your own as PubNub provides a mechanism to target only the devices registered to PubNub channels.
:::

If you include a `topic` field in your payload, FCM throws an error when spotting more than one `target` field.

```text
FCM JSON payload error: Invalid value at 'message.data[0].value' (TYPE_STRING), 1234
Invalid JSON payload received. Unknown name "nick" at 'message.data[1].value': Cannot find field.
Invalid value at 'message' (oneof), oneof field 'target' is already set. Cannot set 'token'
```

##### Metadata in message payloads

Add metadata to pass custom information to the app. For FCM, include key‑value pairs in the `data` field.

```json
{
  "data": {
    "userId": "12345",
    "action": "openProfile"
  }
}
```

#### Prevent self-notifications

Exclude the sender’s device token to avoid a push for the sender. Add the token(s) to `pn_fcm.pn_exceptions`:

```json
{
  "pn_fcm": {
    "notification": {
      "title": "Chat Invitation",
      "body": "John invited you to chat"
    },
    "pn_exceptions": [
      "<sender_fcm_token>",
      "<optional_other_device_token>"
    ]
  }
}
```

#### Examples

Certain PubNub SDKs use helper methods to create a valid FCM payload.

##### Example payload

Read [Firebase Docs](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages) for more details.

```json
{
  "pn_fcm": {
    "notification": {
      "title": "Chat Invitation",
      "body": "John invited you to chat"
    },
      "pn_exceptions" : ["device-token1", "device-token2"]
    }
}
```

##### Java

```java
import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper;
import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper.FCMPayloadV2;
import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper.FCMPayloadV2.Notification;

// Create the notification object
PushPayloadHelper.FCMPayloadV2.Notification notification = new PushPayloadHelper.FCMPayloadV2.Notification();
        notification.setTitle("Chat Invitation");
        notification.setBody("John invited you to chat");

// Create the FCM v2 payload
        PushPayloadHelper.FCMPayloadV2 fcmPayloadV2 = new PushPayloadHelper.FCMPayloadV2();
        fcmPayloadV2.setNotification(notification);

// Use the PushPayloadHelper to build the final payload
        PushPayloadHelper payloadHelper = new PushPayloadHelper();
        payloadHelper.setFcmPayloadV2(fcmPayloadV2);

// Build the payload
        Map<String, Object> payload = payloadHelper.build();
        ((Map<String,Object>)payload.get("pn_fcm")).put("pn_exceptions", Arrays.asList("device-token1", "device-token2"));
```

##### Kotlin

```kotlin
import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper
import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper.FCMPayloadV2
import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper.FCMPayloadV2.Notification

val notification = Notification().apply {
    title = "Chat Invitation"
    body = "John invited you to chat"
}

// Create the FCM v2 payload
val fcmPayloadV2 = FCMPayloadV2().apply {
    this.notification = notification
}

// Use the PushPayloadHelper to deep merge the FCM v2 payload and the common payload
val payloadHelper = PushPayloadHelper().apply {
    this.fcmPayloadV2 = fcmPayloadV2
}

// Build the payload
val payload = payloadHelper.build()

// Add the exceptions
val payloadWithExceptions = payload.toMutableMap().apply {
  val fcmPayload = get("pn_fcm").toMutableMap().apply {
      put("pn_exceptions", listOf("device-token1", "device-token2"))
  }
  put("pn_fcm", fcmPayload)
}
```

##### Swift

```swift
let notification = [
  "title": "Chat invitation",
  "body": "John invited you to chat"
]

let pnFcmDict = [
  "pn_fcm": ["notification": notification],
  "pn_exceptions": ["device-token1", "device-token2"]
] as [String : Any]
```

##### Objective-C

```objectivec
NSString* title = @"Chat Invitation";
NSString* body = @"John invited you to chat";
PNNotificationsPayload* builder = [PNNotificationsPayload payloadsWithNotificationTitle: title body: body];

NSMutableDictionary* dictRepresentation = [[builder dictionaryRepresentationFor:PNFCMPush] mutableCopy];
dictRepresentation[@"pn_exceptions"] = @[@"device-token1", @"device-token2"];
```

##### JavaScript

```javascript
const builder = PubNub.notificationPayload('Chat Invitation', 'John invited you to chat');
const payload = builder.buildPayload(['fcm']);
payload['pn_gcm']['notification']['pn_exceptions'] = ['device-token1', 'device-token2'];
```

## Step 6: Publish the push notification

When your push payload is ready, call `publish` to send the message on a channel. If the message contains `pn_fcm`, PubNub retrieves device tokens registered on the target channel and forwards a push request to FCM.

### Java

```java
pubnub.publish()
  .channel("ch1")
  .message(pushPayload)
  .async(result -> { /* check result */ });
```

### Kotlin

```kotlin
pubnub.publish(
  channel = "ch1",
  message = pushPayload
).async { result ->
  // Handle Response
}
```

### Swift

```swift
let notification = [
  "title": "Chat invitation",
  "body": "John invited you to chat"
]

let pnFcmDict = [
  "pn_fcm": ["notification": notification],
  "pn_exceptions": ["device-token1", "device-token2"]
] as [String : Any]

pubnub.publish(channel: "channel", message: AnyJSON(pnFcmDict)) {
  switch $0 {
  case .success(let timetoken):
    debugPrint("Message has been successfully published at \(timetoken) timetoken")
  case .failure(let error):
    debugPrint("Did fail to publish due to error \(error)")
  }
}
```

### Objective-C

```objectivec
NSString* title = @"Chat Invitation";
NSString* body = @"John invited you to chat";
PNNotificationsPayload* builder = [PNNotificationsPayload payloadsWithNotificationTitle: title body: body];

NSMutableDictionary* dictRepresentation = [[builder dictionaryRepresentationFor:PNFCMPush] mutableCopy];
dictRepresentation[@"pn_exceptions"] = @[@"device-token1", @"device-token2"];

[self.client publish:dictRepresentation toChannel:@"test-channel" withCompletion:^(PNPublishStatus* status) {
  NSLog(@"Did publish a message with status %@", status);
}];
```

### JavaScript

```javascript
//publish on channel
pubnub.publish(
  {
    channel: "ch1"
    message: pushPayload
  },
  function (status, response) {
    // Handle Response
  }
);
```

For more information about push troubleshooting, refer to [Mobile Push Troubleshooting](https://www.pubnub.com/docs/general/push/mobile-push-troubleshooting).