> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.onesignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Mobile service extensions

> Intercept and customize push notifications before display on iOS and Android. Enable rich media, confirmed receipt, custom styling, and background data handling.

Notification service extensions let you intercept and modify push notifications before they are displayed to the user. Use them to add rich media, customize appearance, handle background data, and enable confirmed receipt analytics.

<Note>
  For a full reference of the notification payload structure, see [OSNotification payload](./osnotification-payload).
</Note>

## Android notification service extension

The Android notification service extension allows you to process notifications before they are shown to the user. Common use cases include:

* Receiving data in the background with or without displaying a notification
* Overriding notification settings based on client-side app logic, such as custom accent color, vibration pattern, or other [`NotificationCompat`](https://developer.android.com/reference/androidx/core/app/NotificationCompat) options

### Step 1: Create a class for the service extension

Create a class that implements `INotificationServiceExtension` and its `onNotificationReceived` method.

The `onNotificationReceived` method receives an `event` parameter of type [`INotificationReceivedEvent`](https://github.com/OneSignal/OneSignal-Android-SDK/blob/25924dc3739fbe3ae64a73efc7b504449a18cdea/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/notifications/INotificationReceivedEvent.kt#L46).

<CodeGroup>
  ```java Java theme={null}
  package your.package.name

  import androidx.annotation.Keep;
  import com.onesignal.notifications.IActionButton;
  import com.onesignal.notifications.IDisplayableMutableNotification;
  import com.onesignal.notifications.INotificationReceivedEvent;
  import com.onesignal.notifications.INotificationServiceExtension;

  @Keep
  public class NotificationServiceExtension implements INotificationServiceExtension {

       @Override
       public void onNotificationReceived(INotificationReceivedEvent event) {
          IDisplayableMutableNotification notification = event.getNotification();

          if (notification.getActionButtons() != null) {
             for (IActionButton button : notification.getActionButtons()) {
                // Modify action buttons here
             }
          }
       }
  }
  ```

  ```kotlin Kotlin theme={null}
  package your.package.name

  import androidx.annotation.Keep
  import com.onesignal.notifications.INotificationReceivedEvent
  import com.onesignal.notifications.INotificationServiceExtension

  @Keep
  class NotificationServiceExtension : INotificationServiceExtension {
      override fun onNotificationReceived(event: INotificationReceivedEvent) {
          val notification = event.notification
          val context = event.context

          notification.actionButtons?.forEach { button ->
              // Modify action buttons here
          }
      }
  }
  ```
</CodeGroup>

<Note>
  The `@Keep` annotation is required in both Java and Kotlin to prevent code shrinking tools (R8 or ProGuard) from renaming or removing your class when minification is enabled.
</Note>

### Step 2: Customize the notification

Add your customization logic inside the `onNotificationReceived` method before registering the extension. The following tabs show common patterns.

<Tabs>
  <Tab title="Prevent display">
    Use `event.preventDefault()` to suppress automatic display, then call `display()` after performing async work.

    <CodeGroup>
      ```java Java theme={null}
      event.preventDefault();

      new Thread(() -> {
          try {
              Thread.sleep(1000);
          } catch (InterruptedException ignored) {}

          event.getNotification().display();
      }).start();
      ```

      ```kotlin Kotlin theme={null}
      event.preventDefault()

      Thread {
          try {
              Thread.sleep(1000)
          } catch (ignored: InterruptedException) {}

          event.notification.display()
      }.start()
      ```
    </CodeGroup>

    <Warning>
      If you call `event.preventDefault()` but never call `display()`, the notification is silently dropped. See [Duplicated notifications](./duplicated-notifications#android-notification-service-extension) for details on avoiding duplicate displays.
    </Warning>
  </Tab>

  <Tab title="Add a custom field">
    <CodeGroup>
      ```java Java theme={null}
      String promoCode = notification.getAdditionalData() != null
          ? notification.getAdditionalData().optString("promo_code", null)
          : null;

      if (promoCode != null) {
          String updatedBody = notification.getBody() + " Use code: " + promoCode;
          notification.setExtender(builder -> {
              builder.setContentText(updatedBody);
          });
      }
      ```

      ```kotlin Kotlin theme={null}
      val promoCode = notification.additionalData?.optString("promo_code", null)

      promoCode?.let {
          val updatedBody = "${notification.body}\nUse code: $promoCode"
          notification.setExtender { builder ->
              builder.setContentText(updatedBody)
          }
      }
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Change color and icon">
    <CodeGroup>
      ```java Java theme={null}
      int iconResId = R.drawable.icon_default;
      String type = notification.getAdditionalData() != null
          ? notification.getAdditionalData().optString("type", "")
          : "";

      switch (type) {
          case "sale":
              iconResId = R.drawable.icon_sale;
              break;
          case "reminder":
              iconResId = R.drawable.icon_reminder;
              break;
      }

      int finalIconResId = iconResId;
      notification.setExtender(builder -> {
          builder.setColor(0xFF0000FF).setSmallIcon(finalIconResId);
      });
      ```

      ```kotlin Kotlin theme={null}
      val type = notification.additionalData?.optString("type", "") ?: ""

      val iconResId = when (type) {
          "sale" -> R.drawable.icon_sale
          "reminder" -> R.drawable.icon_reminder
          else -> R.drawable.icon_default
      }

      notification.setExtender { builder ->
          builder.setColor(0xFF0000FF).setSmallIcon(iconResId)
      }
      ```
    </CodeGroup>

    <Note>
      Icons referenced here must exist in the `res/drawable` directory.
    </Note>
  </Tab>
</Tabs>

### Step 3: Add the service extension to your AndroidManifest.xml

Add the class name and value as `meta-data` within the `<application>` tag of your `AndroidManifest.xml` file. Ignore any "unused" warnings.

```xml XML theme={null}
<application>
  <meta-data
    android:name="com.onesignal.NotificationServiceExtension"
    android:value="com.onesignal.example.NotificationServiceExtension" />
</application>
```

Replace `com.onesignal.example.NotificationServiceExtension` with your class's fully qualified name.

***

## iOS notification service extension

The [`UNNotificationServiceExtension`](https://developer.apple.com/reference/usernotifications/unnotificationserviceextension) allows you to modify push notification content before it is displayed. It is required for:

* [Images & rich media](./rich-media)
* [Confirmed receipt](./confirmed-delivery)
* [Badges](./badges)
* [Action buttons](./action-buttons)
* [Influenced opens with Firebase Analytics](./google-analytics-for-firebase)

If you followed the [Mobile SDK setup](./mobile-sdk-setup), the notification service extension is already configured. This section explains how to access the OneSignal notification payload data and troubleshoot issues.

### Getting the iOS push payload

Inside your `didReceive(_:withContentHandler:)` override, you call `OneSignalExtension.didReceiveNotificationExtensionRequest` to let OneSignal process the notification and invoke the content handler. Read or modify `bestAttemptContent` before calling this method.

In this example, a notification is sent with the following data:

```json JSON theme={null}
{
  "app_id": "YOUR_APP_ID",
  "target_channel": "push",
  "headings": {"en": "The message title"},
  "contents": {"en": "The message contents"},
  "data":{
    "additional_data_key_1":"value_1",
    "additional_data_key_2":"value_2"
    },
  "include_subscription_ids": ["SUBSCRIPTION_ID_1"]
}
```

Access this additional `data` within the OneSignalNotificationServiceExtension via the `a` key inside the `custom` dictionary of `userInfo`:

<CodeGroup>
  ```swift Swift theme={null}
  if let bestAttemptContent = bestAttemptContent {

      if let customData = bestAttemptContent.userInfo["custom"] as? [String: Any],
         let additionalData = customData["a"] as? [String: Any] {

          if let jsonData = try? JSONSerialization.data(withJSONObject: additionalData, options: .prettyPrinted),
             let jsonString = String(data: jsonData, encoding: .utf8) {
              print("The additionalData dictionary in JSON format:\n\(jsonString)")
          } else {
              print("Failed to convert additionalData to JSON format.")
          }
      }

      if let messageData = bestAttemptContent.userInfo["aps"] as? [String: Any],
         let apsData = messageData["alert"] as? [String: Any],
         let body = apsData["body"] as? String,
         let title = apsData["title"] as? String {
          print("The message contents is: \(body), message headings is: \(title)")
      } else {
          print("Unable to retrieve apsData")
      }

      OneSignalExtension.didReceiveNotificationExtensionRequest(self.receivedRequest,
                                                                with: bestAttemptContent,
                                                                withContentHandler: self.contentHandler)
  }
  ```

  ```objc Objective-C theme={null}
  if (bestAttemptContent) {
      NSDictionary *customData = bestAttemptContent.userInfo[@"custom"];
      if ([customData isKindOfClass:[NSDictionary class]]) {
          NSDictionary *additionalData = customData[@"a"];
          if ([additionalData isKindOfClass:[NSDictionary class]]) {
              NSError *error;
              NSData *jsonData = [NSJSONSerialization dataWithJSONObject:additionalData options:NSJSONWritingPrettyPrinted error:&error];
              if (jsonData) {
                  NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
                  NSLog(@"The additionalData dictionary in JSON format:\n%@", jsonString);
              } else {
                  NSLog(@"Failed to convert additionalData to JSON format: %@", error.localizedDescription);
              }
          }
      }

      NSDictionary *messageData = bestAttemptContent.userInfo[@"aps"];
      if ([messageData isKindOfClass:[NSDictionary class]]) {
          NSDictionary *apsData = messageData[@"alert"];
          if ([apsData isKindOfClass:[NSDictionary class]]) {
              NSString *body = apsData[@"body"];
              NSString *title = apsData[@"title"];
              if ([body isKindOfClass:[NSString class]] && [title isKindOfClass:[NSString class]]) {
                  NSLog(@"The message content is: %@, message heading is: %@", body, title);
              }
          } else {
              NSLog(@"Unable to retrieve apsData");
          }
      }

      [OneSignalExtension didReceiveNotificationExtensionRequest:self.receivedRequest
                                               withNotification:bestAttemptContent
                                                withContentHandler:self.contentHandler];
  }
  ```
</CodeGroup>

**Example console output:**

```
The additionalData dictionary in JSON format:
{
  "additional_data_key_1" : "value_1",
  "additional_data_key_2" : "value_2"
}
The message contents is: The message contents, message headings is: The message title
```

### Troubleshooting the iOS notification service extension

Use this section to debug issues with images, action buttons, or confirmed deliveries not showing on iOS.

#### Check your Xcode settings

In **General > Targets**, verify that your **main app target** and **OneSignalNotificationServiceExtension** target have the same:

* **Supported Destinations**
* **Minimum Deployment** (iOS 14.5 or higher)

<Warning>
  If you are using CocoaPods, make sure these match with your main target in the Podfile to avoid build errors.
</Warning>

<Frame caption="Example main app target in Xcode">
  <img src="https://mintcdn.com/onesignal/RWA35uTjv8voG5iC/images/mobile/main-app-target-general-settings.png?fit=max&auto=format&n=RWA35uTjv8voG5iC&q=85&s=eb9dd4d9dbec5f2b9f351ff168087557" alt="Xcode General tab showing Supported Destinations and Minimum Deployment for the main app target" width="2988" height="1824" data-path="images/mobile/main-app-target-general-settings.png" />
</Frame>

<Frame caption="Example OneSignalNotificationServiceExtension target in Xcode">
  <img src="https://mintcdn.com/onesignal/x4RdPY-EcasyyQ-o/images/mobile/onesignal-notification-service-extension-target-general-settings.png?fit=max&auto=format&n=x4RdPY-EcasyyQ-o&q=85&s=ea6f697fda7b4a338bb7e3a4dac3856f" alt="Xcode General tab showing Supported Destinations and Minimum Deployment for the notification service extension target" width="2988" height="1824" data-path="images/mobile/onesignal-notification-service-extension-target-general-settings.png" />
</Frame>

In the **OneSignalNotificationServiceExtension > Info** tab, expand the `NSExtension` key and verify it contains:

```xml XML theme={null}
<dict>
  <key>NSExtensionPointIdentifier</key>
  <string>com.apple.usernotifications.service</string>
  <key>NSExtensionPrincipalClass</key>
  <string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
```

<Frame caption="Example NSExtension key in the Info tab">
  <img src="https://mintcdn.com/onesignal/RWA35uTjv8voG5iC/images/mobile/onesignal-notification-service-extension-info-tab.png?fit=max&auto=format&n=RWA35uTjv8voG5iC&q=85&s=06e65f878427e94127ef7391903d583e" alt="Xcode Info tab showing the NSExtension dictionary with NSExtensionPointIdentifier and NSExtensionPrincipalClass keys" width="3282" height="1888" data-path="images/mobile/onesignal-notification-service-extension-info-tab.png" />
</Frame>

<Warning>
  If using Objective-C, replace `$(PRODUCT_MODULE_NAME).NotificationService` with `NotificationService`.
</Warning>

#### Turn off "Copy only when installing"

Select your **main app target > Build Phases > Embed App Extensions**. Ensure "Copy only when installing" is **not** checked:

<Frame caption="Main app target build phase settings">
  <img src="https://mintcdn.com/onesignal/RWA35uTjv8voG5iC/images/mobile/main-app-target-build-phases.png?fit=max&auto=format&n=RWA35uTjv8voG5iC&q=85&s=cd507862b8e5bb0934b6f6b082d0b26e" alt="Xcode Build Phases tab showing Embed App Extensions with Copy only when installing unchecked" width="3282" height="1888" data-path="images/mobile/main-app-target-build-phases.png" />
</Frame>

### Debugging the iOS notification service extension

Follow these steps to verify the notification service extension is set up correctly.

#### 1. Update the OneSignalNotificationServiceExtension code

Open `NotificationService.m` or `NotificationService.swift` and replace the file contents with the code below. This adds logging to help verify the extension is running.

Replace `YOUR_BUNDLE_ID` with your actual Bundle ID.

<CodeGroup>
  ```swift Swift theme={null}
  import UserNotifications
  import OneSignalExtension
  import os.log

  class NotificationService: UNNotificationServiceExtension {

      var contentHandler: ((UNNotificationContent) -> Void)?
      var receivedRequest: UNNotificationRequest!
      var bestAttemptContent: UNMutableNotificationContent?

      override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
          self.receivedRequest = request
          self.contentHandler = contentHandler
          self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

          let userInfo = request.content.userInfo
          let custom = userInfo["custom"]
          print("Running NotificationServiceExtension: userInfo = \(userInfo.description)")
          print("Running NotificationServiceExtension: custom = \(custom.debugDescription)")
          os_log("%{public}@", log: OSLog(subsystem: "YOUR_BUNDLE_ID", category: "OneSignalNotificationServiceExtension"), type: OSLogType.debug, userInfo.debugDescription)

          if let bestAttemptContent = bestAttemptContent {
              // Prepend "[Modified]" to confirm the extension is running
              print("Running NotificationServiceExtension")
              bestAttemptContent.body = "[Modified] " + bestAttemptContent.body

              OneSignalExtension.didReceiveNotificationExtensionRequest(self.receivedRequest, with: bestAttemptContent, withContentHandler: self.contentHandler)
          }
      }

      override func serviceExtensionTimeWillExpire() {
          if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
              OneSignalExtension.serviceExtensionTimeWillExpireRequest(self.receivedRequest, with: self.bestAttemptContent)
              contentHandler(bestAttemptContent)
          }
      }
  }
  ```

  ```objc Objective-C theme={null}
  #import <OneSignalExtension/OneSignalExtension.h>

  #import "NotificationService.h"

  @interface NotificationService ()

  @property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
  @property (nonatomic, strong) UNNotificationRequest *receivedRequest;
  @property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;

  @end

  @implementation NotificationService

  - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
      self.receivedRequest = request;
      self.contentHandler = contentHandler;
      self.bestAttemptContent = [request.content mutableCopy];

      NSUserDefaults *userDefault = [[NSUserDefaults alloc] initWithSuiteName:@"group.YOUR_BUNDLE_ID.onesignal"];
      NSLog(@"NSE player_id: %@", [userDefault stringForKey:@"GT_PLAYER_ID"]);
      NSLog(@"NSE app_id: %@", [userDefault stringForKey:@"GT_APP_ID"]);

      // Prepend "[Modified]" to confirm the extension is running
      NSLog(@"Running NotificationServiceExtension");
      self.bestAttemptContent.body = [@"[Modified] " stringByAppendingString:self.bestAttemptContent.body];

      [OneSignal.Debug setLogLevel:ONE_S_LL_VERBOSE];

      [OneSignalExtension didReceiveNotificationExtensionRequest:self.receivedRequest
                                                withNotification:self.bestAttemptContent
                                              withContentHandler:self.contentHandler];
  }

  - (void)serviceExtensionTimeWillExpire {
      [OneSignalExtension serviceExtensionTimeWillExpireRequest:self.receivedRequest
                                               withNotification:self.bestAttemptContent];

      self.contentHandler(self.bestAttemptContent);
  }

  @end
  ```
</CodeGroup>

<Note>
  Debug log types need to be enabled in Console via **Action > Include Debug Messages**.
</Note>

#### 2. Change your active scheme

Set your Active Scheme to `OneSignalNotificationServiceExtension`.

<Frame caption="Xcode active scheme selection">
  <img src="https://mintcdn.com/onesignal/x4RdPY-EcasyyQ-o/images/mobile/xcode-active-scheme-selection.png?fit=max&auto=format&n=x4RdPY-EcasyyQ-o&q=85&s=27407135e39bc606a02e005efd3a56ff" alt="Xcode toolbar showing the active scheme dropdown set to OneSignalNotificationServiceExtension" width="3282" height="1888" data-path="images/mobile/xcode-active-scheme-selection.png" />
</Frame>

#### 3. Build and run the project

Build and run the project in Xcode on a real device.

#### 4. Open the console

In Xcode, select **Window > Devices and Simulators**.

<Frame caption="Xcode Devices and Simulators window">
  <img src="https://mintcdn.com/onesignal/x4RdPY-EcasyyQ-o/images/mobile/xcode-devices-and-simulators-selection.png?fit=max&auto=format&n=x4RdPY-EcasyyQ-o&q=85&s=2bc1305ee0fd45087f33a078a7aeaffb" alt="Xcode menu showing the Window dropdown with Devices and Simulators selected" width="2518" height="1374" data-path="images/mobile/xcode-devices-and-simulators-selection.png" />
</Frame>

You should see your device connected. Select **Open Console**.

<Frame caption="Device console access button">
  <img src="https://mintcdn.com/onesignal/x4RdPY-EcasyyQ-o/images/mobile/xcode-device-console-access-button.png?fit=max&auto=format&n=x4RdPY-EcasyyQ-o&q=85&s=7ffa64afd28d452e200241ad4751c102" alt="Xcode Devices window showing the Open Console button for a connected device" width="2304" height="1624" data-path="images/mobile/xcode-device-console-access-button.png" />
</Frame>

#### 5. Check the console

In the Console:

1. Select **Action > Include Debug Messages**
2. Search for `OneSignalNotificationServiceExtension` as the CATEGORY
3. Select **Start**

<Frame caption="Console debugging configuration">
  <img src="https://mintcdn.com/onesignal/x4RdPY-EcasyyQ-o/images/mobile/xcode-console-debugging-configuration.png?fit=max&auto=format&n=x4RdPY-EcasyyQ-o&q=85&s=82593a0287811e6b787cee686ab7196d" alt="macOS Console app showing the category filter and Start button for debugging the notification service extension" width="3368" height="1232" data-path="images/mobile/xcode-console-debugging-configuration.png" />
</Frame>

Send a notification to the device with a message body (use the `contents` property if sending from the [Create notification](/reference/push-notification) API). Example payload:

```curl cURL theme={null}
curl --request POST \
 --url 'https://api.onesignal.com/notifications' \
 --header 'Authorization: Key YOUR_API_KEY' \
 --header 'accept: application/json' \
 --header 'content-type: application/json' \
 --data '
{
"app_id": "YOUR_APP_ID",
"target_channel": "push",
"headings": {"en": "The message title"},
"contents": {"en": "The message contents"},
"data":{"additional_data_key_1":"value_1","additional_data_key_2":"value_2"},
"include_subscription_ids": [
"SUBSCRIPTION_ID_1"
]
}'
```

You should see a message logged whether the app is running or not.

<Frame caption="Console debug output example">
  <img src="https://mintcdn.com/onesignal/x4RdPY-EcasyyQ-o/images/mobile/xcode-console-debug-output.png?fit=max&auto=format&n=x4RdPY-EcasyyQ-o&q=85&s=d93e3fc36610941e1c01b83abc0234b3" alt="macOS Console app showing debug log output from the OneSignalNotificationServiceExtension" width="3604" height="1776" data-path="images/mobile/xcode-console-debug-output.png" />
</Frame>

If you do not see a message, remove OneSignal from your app and follow the [Mobile SDK setup](./mobile-sdk-setup) again to verify the integration.

## FAQ

### Why isn't my notification service extension running on iOS?

The extension only runs when `mutable-content` is set in the push payload. OneSignal sets this automatically when you include an attachment or action buttons. Verify your Xcode settings match the requirements in the [troubleshooting section](#troubleshooting-the-ios-notification-service-extension).

### Can I prevent a notification from displaying on Android?

Yes. Call `event.preventDefault()` in your `onNotificationReceived` method to suppress automatic display. You can then call `event.getNotification().display()` to show it later, or never call `display()` to silently drop the notification without displaying it. See [Duplicated notifications](./duplicated-notifications#android-notification-service-extension) for details on avoiding duplicates when using this pattern.

### Do I need the @Keep annotation on Android?

Yes. The `@Keep` annotation prevents ProGuard or R8 from renaming or removing your class that implements `INotificationServiceExtension` during minification. Without it, OneSignal cannot find the class at runtime.

## Related pages

<Columns cols={2}>
  <Card title="Mobile SDK setup" icon="mobile" href="./mobile-sdk-setup">
    Install and configure the OneSignal SDK for iOS and Android.
  </Card>

  <Card title="Images & rich media" icon="image" href="./rich-media">
    Attach images, GIFs, and video to push notifications.
  </Card>

  <Card title="Confirmed receipt" icon="check" href="./confirmed-delivery">
    Track confirmed notification delivery to devices.
  </Card>

  <Card title="Duplicated notifications" icon="clone" href="./duplicated-notifications">
    Troubleshoot duplicate push notifications on all platforms.
  </Card>
</Columns>
