Migrate from FCM HTTP v1 API to AndroidConfigV2

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.

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) and request the https://www.googleapis.com/auth/firebase.messaging OAuth 2.0 scope when minting access tokens:

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)

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, 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

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

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().

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 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 branch of firebase/firebase-android-sdk, build the local Maven repository, and copy the generated build/m2repository directory into your Android project root:

      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

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

    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

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

    Groovy

    dependencies {
        implementation 'com.google.firebase:firebase-messaging:26.1.0-eap01'
    }
    
  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

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

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

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

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

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

@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();
}

Handle BackgroundSync messages using onBackgroundSyncReceived

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

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

@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.
}

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

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

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.

Understand notification suppression and timeouts

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.