> [!NOTE]
> **Note:** This feature is a **preview** release and is subject to the "Pre-GA Offerings Terms" in the General Service Terms section of the [Service Specific Terms](https://docs.cloud.google.com/terms/service-terms#1). Pre-GA products and features are available "as is" and might have limited support. For more information, see the [launch stage descriptions](https://cloud.google.com/products/#product-launch-stages).

Using the FCM `AndroidConfigV2`, you can send intent
driven messages to Android devices with timely notification delivery and a
simplified API.

This guide covers how to migrate your backend send requests from `AndroidConfig`
to `AndroidConfigV2` and how to update your Android application
clients from legacy `onMessageReceived` handling to the new v2 client
callbacks `onNotificationReceived` and `onBackgroundSyncReceived`.

> [!TIP]
> **Tip:** **Backward compatibility** : `AndroidConfigV2` is supported on all Android versions. When your server sends `AndroidConfigV2`, FCM automatically translates the message for devices running older client SDKs. You can update your server-side message sending logic without waiting for client app updates.

## What's new in AndroidConfigV2

The
FCM AndroidConfigV2 API simplifies developer experience by introducing
two message
types:

- **`RemoteNotification`** : Recommended for all FCM messages that display an urgent, time-sensitive user-visible notification. These messages bypass any battery-saving measures and are delivered without delay. On the device, the FCM SDK and Google Play services take ownership of posting the notification to Android's `NotificationManager`.
- **`BackgroundSync`**: Non-urgent data messages intended for background synchronization and app state updates. Delivery can be batched or delayed to optimize battery life.

| Feature | Legacy (`AndroidConfig` v1) | New (`AndroidConfigV2`) |
|---|---|---|
| **Configuration field** | `message.android` | `message.android_v2` |
| **Priority definition** | Explicit `priority: "high"` or `priority: "normal"` | Implied by union type (`remote_notification` for urgent alerts versus `background_sync` for non-urgent sync) |
| **Notification posting** | App manually calls `NotificationManager.notify()` | FCM SDK and GMS Core posts directly or using `NotificationResponse` |
| **Client callbacks** | • `onMessageReceived(RemoteMessage)` • `onDeletedMessages()` | • `onNotificationReceived(RemoteMessage)` • `onBackgroundSyncReceived(RemoteMessage)` • `onDeletedMessages()` |
| **Collapsibility** | Varied; required explicit setting | Both message types can be collapsible or non-collapsible (not collapsible by default) |

## Authorize AndroidConfigV2 send requests

You can continue to use OAuth 2.0 access tokens derived from your Firebase
service account using Google Application Default Credentials or the Admin SDK,
and send `POST` requests to the HTTP v1 `messages:send` endpoint.

If you're not using your Firebase service account, make sure to assign your dedicated account to the [**Firebase Cloud Messaging API Admin** role (`roles/firebase.messagingAdmin`)](https://cloud.google.com/iam/docs/roles-permissions/firebasecloudmessaging#firebasecloudmessaging.admin)
and request the `https://www.googleapis.com/auth/firebase.messaging` OAuth 2.0
scope when minting access tokens:

```console
POST https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send HTTP/1.1
Authorization: Bearer YOUR_OAUTH_ACCESS_TOKEN
Content-Type: application/json
```

## Update server send requests (REST)

To send an `AndroidConfigV2` request using the REST API, replace the `android`
object in your request body with `android_v2`. Within `android_v2`, specify
common parameters (such as `ttl`, `collapse_key`, `data`, and
`restricted_package_name`), and provide one of the following union fields:
`remote_notification` or `background_sync`.

### Send a RemoteNotification message

To send an urgent, user-facing notification, include the `remote_notification`
object and its recommended `notification` payload. By default `mutable_content`
is set to false. The GMS Core immediately renders and posts the notification to
the system tray without executing background app code.

#### Complete HTTP request JSON sample (Auto-rendered notification)

```console
POST https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1
Content-Type: application/json
Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA

{
  "message": {
    "token": "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "notification": {
      "title": "FCM Message",
      "body": "This is an FCM notification message!"
    },
    "android_v2": {
      "ttl": "3600s",
      "remote_notification": {
        "mutable_content": false,
        "notification": {
          "title": "Order update",
          "body": "Your order has been shipped!",
          "icon": "ic_notification_custom",
          "sound": "default",
          "tag": "alert_tag_1",
          "id": 100
        }
      }
    }
  }
}
```

### Send a RemoteNotification with mutable content

If your application needs to modify the notification on the device before it is
displayed, set `mutable_content: true` inside `remote_notification`. This
instructs GMS Core to bind to your client application and invoke
`onNotificationReceived()`. This is helpful when you want to [decrypt an
end-to-end encrypted payload](https://firebase.google.com/docs/cloud-messaging/send/v2-api#e2ee-notification-flow), attach user avatars, or
localize strings.

Make sure to include a `notification` object inside `remote_notification` even
when `mutable_content` is `true`. GMS Core uses this `notification` object as
the fallback notification if the client callback times out or exceeds
suppression limits.

If you're migrating from legacy v1 data messages and want FCM to
deliver the payload to older v1 client installations as a pure data message, set
`use_as_v1_data_message: true` inside `remote_notification`.

#### Complete HTTP request JSON sample

```console
POST https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1
Content-Type: application/json
Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA

{
  "message": {
    "token": "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "data": {
      "encrypted_content": "U2FsdGVkX19vN2m+",
      "sender_id": "user_98124"
    },
    "android_v2": {
      "ttl": "3600s",
      "remote_notification": {
        "mutable_content": true,
        "use_as_v1_data_message": true,
        "notification": {
          "title": "New message",
          "body": "You have a new message",
          "tag": "chat_alert",
          "id": 1001
        }
      }
    }
  }
}
```

### Send a BackgroundSync message

To send a non-urgent message for background data synchronization, silent state
updates, or updating an app badge or widget, use the `background_sync` object.
Don't use these messages to post
user-visible notifications, as these can be batched or delayed to optimize
device battery life.

#### Complete HTTP request JSON sample

```console
POST https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1
Content-Type: application/json
Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA

{
  "message": {
    "token": "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "data": {
      "score": "FETCH_UPDATES",
      "sync_action": "FETCH_UPDATES"
    },
    "android_v2": {
      "ttl": "86400s",
      "collapse_key": "app_state_sync",
      "background_sync": {}
    }
  }
}
```

## Update Android client callbacks

To receive `AndroidConfigV2` messages on Android, upgrade your app to use the
latest `firebase-messaging` SDK. Update your service extending
`FirebaseMessagingService` by implementing `onNotificationReceived()` and
`onBackgroundSyncReceived()`.

> [!NOTE]
> **Note:** If a device receives an `AndroidConfigV2` message before your app overrides `onNotificationReceived()` or `onBackgroundSyncReceived()`, GMS Core detects that the v2 callbacks are unimplemented and automatically falls back to delivering the message through the legacy v1 `onMessageReceived()` broadcast.

### Install the EAP SDK

During the EAP, the `firebase-messaging` v2 SDK is distributed as a local Maven
repository archive rather than through Google's public Maven repository.

1. Add the `m2repository` directory containing `firebase-messaging:26.1.0-eap01`
   to the root directory of your Android project (`<project-root>/m2repository`)
   using one of the following options:

   1. **Option A: Download the prebuilt archive** : Download [FirebaseMessaging_26.1.0-eap01.zip](https://dl.google.com/firebase/sdk/android/FirebaseMessaging_26.1.0-eap01.zip) and extract the `m2repository` directory into the root directory of your Android project (`<project-root>/m2repository`).
   2. **Option B: Build from GitHub with Gradle** : Clone the
      [`feature/fcm-android-config-v2-eap`](https://github.com/firebase/firebase-android-sdk/tree/feature/fcm-android-config-v2-eap)
      branch of `firebase/firebase-android-sdk`, build the local Maven
      repository, and copy the generated `build/m2repository` directory into
      your Android project root:

      ```bash
      git clone --branch feature/fcm-android-config-v2-eap --single-branch \
        https://github.com/firebase/firebase-android-sdk.git
      cd firebase-android-sdk
      ./gradlew -PprojectsToPublish="firebase-messaging" firebasePublish
      cp -R build/m2repository /path/to/your/android-project/m2repository
      ```
2. In your project-level `settings.gradle.kts` (or `settings.gradle`), add the
   local `m2repository` directory to the top of your `repositories` block,
   above `google()`:

   ### Kotlin

   ```kotlin
   dependencyResolutionManagement {
       repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
       repositories {
           maven { url = uri("${rootDir}/m2repository") }
           google()
           mavenCentral()
       }
   }
   ```

   ### Groovy

   ```groovy
   dependencyResolutionManagement {
       repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
       repositories {
           maven { url "${rootDir}/m2repository" }
           google()
           mavenCentral()
       }
   }
   ```
3. In your module-level `app/build.gradle.kts` (or `app/build.gradle`), update
   your `firebase-messaging` dependency to `26.1.0-eap01`:

   ### Kotlin

   ```kotlin
   dependencies {
       implementation("com.google.firebase:firebase-messaging:26.1.0-eap01")
   }
   ```

   ### Groovy

   ```groovy
   dependencies {
       implementation 'com.google.firebase:firebase-messaging:26.1.0-eap01'
   }
   ```

   > [!NOTE]
   > **Note:** If your app uses the Firebase Android BoM (`firebase-bom`), you must still specify the explicit `26.1.0-eap01` version on `firebase-messaging` so Gradle overrides the version managed by the BoM.

4. Sync your project with Gradle files.

### Handle RemoteNotifications with `onNotificationReceived`

With `AndroidConfigV2`, `RemoteNotification` messages are displayed
unless `mutable_content` is set to true or they're explicitly silenced in
`onNotificationReceived()`.

When a `RemoteNotification` arrives with `mutable_content: true`, GMS Core binds
to your application and calls `onNotificationReceived()`. Your method must
return a `NotificationResponse` within 20 seconds.

#### Return a modified notification

The `Notification` object you pass to `NotificationResponse.create` replaces the
server's `remote_notification.notification` fallback payload properties defined
in the send request. To keep values from the send request, read them from
`message.notification` (`message.getNotification()` in Java) while applying
on-device modifications:

### Kotlin

```kotlin
import androidx.core.app.NotificationCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.NotificationResponse
import com.google.firebase.messaging.RemoteMessage

class MyFirebaseMessagingService : FirebaseMessagingService() {

    override fun onNotificationReceived(message: RemoteMessage): NotificationResponse {
        val serverNotif = message.notification
        val channelId = serverNotif?.channelId ?: "default_channel"
        // Keep the server-defined title unchanged, or fall back to default
        val title = serverNotif?.title ?: "New message"
        // Modify the notification body on-device (for example, decrypting data)
        val decryptedBody = decryptPayload(message.data["encrypted_content"])

        val builder = NotificationCompat.Builder(this, channelId)
            .setSmallIcon(R.drawable.ic_stat_notification)
            .setContentTitle(title)
            .setContentText(decryptedBody)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setAutoCancel(true)

        // Return the response for the open-source FCM SDK to post to the Android platform
        return NotificationResponse.create("optional_tag", 100, builder.build())
    }
}
```

### Java

```java
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import com.google.firebase.messaging.NotificationResponse;
import androidx.core.app.NotificationCompat;

public class MyFirebaseMessagingService extends FirebaseMessagingService {

    @Override
    public NotificationResponse onNotificationReceived(RemoteMessage message) {
        RemoteMessage.Notification serverNotif = message.getNotification();
        String channelId = (serverNotif != null && serverNotif.getChannelId() != null)
                ? serverNotif.getChannelId()
                : "default_channel";
        // Keep the server-defined title unchanged, or fall back to default
        String title = (serverNotif != null && serverNotif.getTitle() != null)
                ? serverNotif.getTitle()
                : "New message";
        // Modify the notification body on-device (for example, decrypting data)
        String decryptedBody = decryptPayload(message.getData().get("encrypted_content"));

        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId)
                .setSmallIcon(R.drawable.ic_stat_notification)
                .setContentTitle(title)
                .setContentText(decryptedBody)
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setAutoCancel(true);

        // Return the response for the open-source FCM SDK to post to the Android platform
        return NotificationResponse.create("optional_tag", 100, builder.build());
    }
}
```

#### Suppress a notification

If the user has the relevant screen or chat active right now, you can suppress
the notification by returning `NotificationResponse.suppress()`:

### Kotlin

```kotlin
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner

override fun onNotificationReceived(message: RemoteMessage): NotificationResponse {
    val chatId = message.data["chat_id"]
    val isAppForeground = ProcessLifecycleOwner.get()
        .lifecycle
        .currentState
        .isAtLeast(Lifecycle.State.STARTED)

    if (isAppForeground && chatRepository.isConversationActive(chatId)) {
        // Suppress notification display because the user is actively viewing this chat
        return NotificationResponse.suppress()
    }

    val builder = buildNotificationForChat(message)
    return NotificationResponse.create("chat_tag", 101, builder.build())
}
```

### Java

```java
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.ProcessLifecycleOwner;

@Override
public NotificationResponse onNotificationReceived(RemoteMessage message) {
    String chatId = message.getData().get("chat_id");
    boolean isAppForeground = ProcessLifecycleOwner.get()
            .getLifecycle()
            .getCurrentState()
            .isAtLeast(Lifecycle.State.STARTED);

    if (isAppForeground && chatRepository.isConversationActive(chatId)) {
        // Suppress notification display because the user is actively viewing this chat
        return NotificationResponse.suppress();
    }

    NotificationCompat.Builder builder = buildNotificationForChat(message);
    return NotificationResponse.create("chat_tag", 101, builder.build());
}
```

#### Stream notifications or cancel older alerts (`FcmNotificationManager`)

If you need to post multiple notifications for a single incoming push, or cancel
a stale notification before displaying a new one, use `FcmNotificationManager`:

### Kotlin

```kotlin
override fun onNotificationReceived(message: RemoteMessage): NotificationResponse {
    // Retrieve the scoped manager for this message execution
    val manager = getFcmNotificationManager(message)

    // 1. Cancel an outdated notification right away
    manager.cancelNow("message_tag", 50)

    // 2. Post the item notification immediately
    val itemNotif = NotificationCompat.Builder(this, "chat_channel")
        .setSmallIcon(R.drawable.ic_message)
        .setContentTitle("Alex")
        .setContentText(message.data["text"])
    manager.postNow("item_tag", 51, itemNotif.build())

    // 3. Post a group summary notification
    val summaryNotif = NotificationCompat.Builder(this, "chat_channel")
        .setSmallIcon(R.drawable.ic_message)
        .setContentTitle("New Conversations")
        .setGroupSummary(true)
    manager.postNow("summary_tag", 52, summaryNotif.build())

    // 4. Return finished() to complete the streaming workflow
    return manager.finished()
}
```

### Java

```java
@Override
public NotificationResponse onNotificationReceived(RemoteMessage message) {
    // Retrieve the scoped manager for this message execution
    FcmNotificationManager manager = getFcmNotificationManager(message);

    // 1. Cancel an outdated notification right away
    manager.cancelNow("message_tag", 50);

    // 2. Post the item notification immediately
    NotificationCompat.Builder itemNotif = new NotificationCompat.Builder(this, "chat_channel")
            .setSmallIcon(R.drawable.ic_message)
            .setContentTitle("Alex")
            .setContentText(message.getData().get("text"));
    manager.postNow("item_tag", 51, itemNotif.build());

    // 3. Post a group summary notification
    NotificationCompat.Builder summaryNotif = new NotificationCompat.Builder(this, "chat_channel")
            .setSmallIcon(R.drawable.ic_message)
            .setContentTitle("New Conversations")
            .setGroupSummary(true);
    manager.postNow("summary_tag", 52, summaryNotif.build());

    // 4. Return finished() to complete the streaming workflow
    return manager.finished();
}
```

> [!CAUTION]
> **Caution:** `FcmNotificationManager` instances are valid only during the execution of `onNotificationReceived()`. You must call `finished()` at the end of the callback and must never invoke `postNow()` or `cancelNow()` from background threads after the method returns.

### Handle BackgroundSync messages using `onBackgroundSyncReceived`

To process `BackgroundSync` messages on Android, use
`onBackgroundSyncReceived()`:

### Kotlin

```kotlin
override fun onBackgroundSyncReceived(message: RemoteMessage) {
    val syncCommand = message.data["sync_action"]

    try {
        // Perform local database synchronization or cache refresh
        AppDataRepository.getInstance(applicationContext).syncWithServer(syncCommand)
    } catch (e: Exception) {
        Log.e("FCM", "Background sync operation failed", e)
    }
    // Execution completes -> GMS Core marks the message as handled and unbinds.
}
```

### Java

```java
@Override
public void onBackgroundSyncReceived(RemoteMessage message) {
    String syncCommand = message.getData().get("sync_action");

    try {
        // Perform local database synchronization or cache refresh
        AppDataRepository.getInstance(getApplicationContext()).syncWithServer(syncCommand);
    } catch (Exception e) {
        Log.e("FCM", "Background sync operation failed", e);
    }
    // Execution completes -> GMS Core marks the message as handled and unbinds.
}
```

> [!NOTE]
> **Note:** `BackgroundSync` messages are intended for silent background updates, such as syncing state, refreshing app badges and widgets. Don't use these messages to post user-visible notifications, as these can be delayed to optimize device battery life.

### Handle pending message deletions using `onDeletedMessages`

In some situations, such as when a device hasn't connected to FCM for
an extended period or when the server queue is full, FCM may delete
pending messages before delivery. In these cases, override
`onDeletedMessages()`:

### Kotlin

```kotlin
override fun onDeletedMessages() {
    // Perform a full state synchronization with your app server
    AppDataRepository.getInstance(applicationContext).scheduleFullSync()
}
```

### Java

```java
@Override
public void onDeletedMessages() {
    // Perform a full state synchronization with your app server
    AppDataRepository.getInstance(getApplicationContext()).scheduleFullSync();
}
```

## Migrate specific apps

### Deliver end-to-end encrypted (E2EE) notifications

When delivering an end-to-end encrypted notification using a
`RemoteNotification`, decrypted notification
content stays within your app and the Android platform:

1. **Deliver mutable notification** : Your server sends a `RemoteNotification` with `mutable_content: true`, the encrypted payload in `data`, and a generic unencrypted backup `notification`. GMS Core delivers the encrypted message to the open-source FCM SDK inside your app.
2. **Decrypt and return notification** : The FCM SDK invokes `onNotificationReceived()`. Your app decrypts the payload locally, builds the final decrypted Android `Notification`, and passes it back to the open-source FCM SDK using `NotificationResponse` or `FcmNotificationManager`.
3. **Post to platform and report metadata** : The open source FCM SDK posts the decrypted notification directly to the Android platform `NotificationManager` API and sends only notification metadata, such as notification IDs and timestamps, back to GMS Core for delivery observability. Decrypted content is never sent to GMS Core.
4. **Post backup notification if necessary** : Only if your app fails to respond within the 20 second timeout or exceeds background suppression limits does GMS Core post the generic backup `notification` directly to the Android platform.

![Architecture diagram showing the 4-step end-to-end encrypted notification delivery flow between GMS Core, the open-source FCM SDK, your app, and the Android platform NotificationManager API.](https://firebase.google.com/static/docs/cloud-messaging/images/e2ee-notification-delivery.png)

## Understand notification suppression and timeouts

> [!NOTE]
> **Note:** Policy parameters such as the 20 second timeout and the 25% suppression threshold may change before General Availability (GA).

To maintain reliable delivery across the Android ecosystem, GMS Core applies
processing timeouts and notification suppression limits when handling
`RemoteNotification` messages:

### Processing timeout

When `mutable_content` is set to `true`, your `onNotificationReceived()` method
must return a `NotificationResponse` within 20 seconds. If
execution exceeds this threshold, the callback is cancelled and GMS Core
automatically posts a fallback notification rendered from the
`remote_notification.notification` payload.

### Notification suppression rate

Since `RemoteNotification` messages bypass Doze restrictions to alert users
immediately and are delivered without delay, applications are expected to
display notifications for received `RemoteNotification` messages.

Every application is permitted to suppress up to
25% of its `RemoteNotification` traffic while running
in the background. If an application's suppression rate exceeds
25%, GMS Core automatically renders and posts a
fallback notification rendered from the `remote_notification.notification`
payload.

Suppressions performed when the application is active in the foreground
don't count toward this threshold. Applications granted
unrestricted background usage permissions are also exempt from background
suppression limits.