# Android SDK Documentation

Generated on: 9/9/2026

This documentation bundle contains the complete Android SDK documentation for use with LLMs and offline reference.

---

## Quick start guide

**Source:** /integrations/sdk/android/quick-start-guide

# Quick start guide

Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations.

Our MCP server can help you get started

Our MCP server includes SDK-installation tools that can help you get integrated quickly with Customer.io and troubleshoot any issues you might have. See [Set up Customer.io MCP](/ai/mcp/get-started/) to get started.

## Setup Process Overview

For native Android apps, our Android SDK sets you up to send push notifications and track user activity. You’ll need to add Customer.io SDK to your project, configure it with your API key, and set up push notification handling. Then, you’ll use the SDK to identify users, track their activity, and respond to incoming notifications.

1.  [Install the SDK.](#install)
2.  [Identify and Track](#identify-and-track)
3.  [Push Notifications](#push-notifications)
4.  [In-App](#in-app)

## 1\. Install the SDK

1.  If you haven’t already gotten a *CDP API key*, you’ll need to add a new *Android* integration to your Customer.io workspace. This “integration” represents your app in Customer.io and provides the CDP API key that you’ll use to initialize the SDK. See [Get your CDP API key](/integrations/sdk/android/getting-started/auth/#get-your-cdp-api-key) for details.
    
2.  Ensure that your `build.gradle` or `settings.gradle` includes the following repositories to resolve dependencies:
    
    ```
    android {
      repositories {
        google()
        mavenCentral()
      }
      ...
    }
    ```
    
3.  Add the following dependencies to your `build.gradle` file to install Customer.io Android SDK:
    
    ```
    dependencies {
      implementation "io.customer.android:datapipelines:4.20.3"
      // Required for push notifications only
      implementation "io.customer.android:messaging-push-fcm:4.20.3"
      // Required for in-app messages only
      implementation "io.customer.android:messaging-in-app:4.20.3"
      // Optional: enrich profiles with device location
      implementation "io.customer.android:location:4.20.3"
      // Optional: send events when people enter or exit a location (requires location)
      implementation "io.customer.android:geofence:4.20.3"
    }
    ```
    
4.  We recommend initializing Customer.io SDK in your app’s `Application` class in the `onCreate` method. This ensures the SDK is accessible throughout your app using `CustomerIO.instance()` method. Use `CustomerIOBuilder` to add and configure the modules you want to enable:
    

KotlinJava

#### Kotlin

```
import io.customer.messaginginapp.MessagingInAppModuleConfig
import io.customer.messaginginapp.ModuleMessagingInApp
import io.customer.messagingpush.ModuleMessagingPushFCM
import io.customer.sdk.CustomerIO
import io.customer.sdk.CustomerIOConfigBuilder
import io.customer.sdk.data.model.Region

val builder = CustomerIOConfigBuilder(applicationContext, "<CDP_API_KEY>")
  // If you're in the EU, set Region.EU. Default is Region.US and optional.
  .region(Region.US)
  // Optional: Enable in-app messaging by adding siteId and Region
  .addCustomerIOModule(
    ModuleMessagingInApp(MessagingInAppModuleConfig.Builder("<SITE_ID>", Region.US).build())
  )
  // Optional: Enable support for push notifications
  .addCustomerIOModule(ModuleMessagingPushFCM())
// Completes setup and initializes the SDK
CustomerIO.initialize(builder.build())
```

#### Java

```
import io.customer.messaginginapp.MessagingInAppModuleConfig;
import io.customer.messaginginapp.ModuleMessagingInApp;
import io.customer.messagingpush.ModuleMessagingPushFCM;
import io.customer.sdk.CustomerIO;
import io.customer.sdk.CustomerIOConfigBuilder;
import io.customer.sdk.data.model.Region;

CustomerIOConfigBuilder builder = new CustomerIOConfigBuilder(this, "<CDP_API_KEY>")
  .region(Region.US.INSTANCE)
  // Optional: Enable in-app messaging by adding siteId and Region
  .addCustomerIOModule(
    // If you're in the EU, set Region.EU
    new ModuleMessagingInApp(new MessagingInAppModuleConfig.Builder("<SITE_ID>", Region.US.INSTANCE).build())
  )
  // Optional: Enable support for push notifications
  .addCustomerIOModule(new ModuleMessagingPushFCM());
// Completes setup and initializes the SDK
CustomerIO.initialize(builder.build());
```

Also track location and geofences

Want to react when someone enters or leaves a real-world place—a store, a venue, a neighborhood? The SDK’s Location and Geofence modules do that. They need location permissions and a little extra setup, so they aren’t part of the steps above. See [geofencing](/integrations/sdk/android/tracking/geofencing/) to get started, or the [packages and configuration reference](/integrations/sdk/android/getting-started/packages-options/) for every option.

## 2\. Identify and Track

1.  Identify a user in your app using the `CustomerIO.instance().identify` method. You *must* identify a user before you can send push notifications and personalized in-app messages.

KotlinJava

#### Kotlin

```
import io.customer.sdk.CustomerIO

fun identifyUserExample() {
  CustomerIO.instance().identify(
    userId = "android-test-user@example.com",
    traits = buildMap {
      put("firstName", "John")
      put("lastName", "Doe")
      put("email", "android-test-user@example.com")
      put("subscriptionStatus", "active")
    },
  )
  Log.d("[CustomerIO]", "User identified successfully")
}
```

#### Java

```
import io.customer.sdk.CustomerIO;

void identifyUserExample() {
    String userId = "android-test-user@example.com";
    Map<String, Object> traits = new HashMap<>();
    traits.put("firstName", "John");
    traits.put("lastName", "Doe");
    traits.put("email", "android-test-user@example.com");
    traits.put("subscriptionStatus", "active");
    CustomerIO.instance().identify(userId, traits);
    Log.d("[CustomerIO]", "User identified successfully");
}
```

1.  Track a custom event using the `CustomerIO.instance().track` method. Events help you trigger personalized automations and track user activity.

KotlinJava

#### Kotlin

```
import io.customer.sdk.CustomerIO

fun trackCustomEventExample() {
  CustomerIO.instance().track(
    name = "purchased_item",
    properties = buildMap {
      put("product", "Premium Subscription")
      put("price", 99.99)
      put("currency", "USD")
    }
  )
  Log.d("[CustomerIO]", "Custom event tracked successfully")
}
```

#### Java

```
import io.customer.sdk.CustomerIO;

void trackCustomEventExample() {
    String name = "purchased_item";
    Map<String, Object> properties = new HashMap<>();
    properties.put("product", "Premium Subscription");
    properties.put("price", 99.99);
    properties.put("currency", "USD");
    CustomerIO.instance().track(name, properties);
    Log.d("[CustomerIO]", "Custom event tracked successfully");
}
```

1.  Track screen views to automatically trigger in-app messages associated with specific screens.

KotlinJava

#### Kotlin

```
import io.customer.sdk.CustomerIO

fun trackScreenViewExample() {
  CustomerIO.instance().screen(
    title = "ProductDetails",
    properties = buildMap {
      put("product_id", "12345")
      put("product_name", "Sample Product")
      put("category", "Electronics")
    }
  )
  Log.d("[CustomerIO]", "Screen view tracked successfully")
}
```

#### Java

```
import io.customer.sdk.CustomerIO;

void trackScreenViewExample() {
    String title = "ProductDetails";
    Map<String, Object> properties = new HashMap<>();
    properties.put("product_id", "12345");
    properties.put("product_name", "Sample Product");
    properties.put("category", "Electronics");
    CustomerIO.instance().screen(title, properties);
    Log.d("[CustomerIO]", "Screen view tracked successfully");
}
```

## 3\. Push Notifications

1.  Set up your push notification credentials by [uploading your Firebase Cloud Messaging (FCM) service account key](/messaging/channels/push/getting-started/#for-android) (.json file) in [Customer.io dashboard](https://fly.customer.io/workspaces/last/settings/actions/push/android)
2.  Add the push messaging dependency to your project:
    
    ```
    implementation "io.customer.android:messaging-push-fcm:4.20.3"
    ```
    
3.  Make sure your app includes required Firebase configurations (like the `google-services` plugin and the `google-services.json` file) as described in the [FCM documentation ↗](https://firebase.google.com/docs/android/setup#add-config-file).
4.  Initialize and include the push module in `CustomerIOBuilder` when setting up the SDK:

KotlinJava

#### Kotlin

```
import io.customer.messagingpush.ModuleMessagingPushFCM

addCustomerIOModule(ModuleMessagingPushFCM())
```

#### Java

```
import io.customer.messagingpush.ModuleMessagingPushFCM;

builder.addCustomerIOModule(new ModuleMessagingPushFCM());
```

1.  Request push notification permissions from the user following [Google’s recommendations ↗](https://developer.android.com/develop/ui/views/notifications/notification-permission).

For more details and customization options, see [Push Notifications](../push/push/).

## 4\. In-App

1.  To enable in-app messaging, add your site ID and Region. You’ll find your site ID under **[Integrations > Customer.io API: Track](https://fly.customer.io/workspaces/last/settings/api_credentials)** in the *Connections* tab.
2.  Add the in-app messaging dependency to your project:
    
    ```
    implementation "io.customer.android:messaging-in-app:4.20.3"
    ```
    
3.  Initialize and include the in-app module in `CustomerIOBuilder` when setting up the SDK:

KotlinJava

#### Kotlin

```
import io.customer.messaginginapp.MessagingInAppModuleConfig
import io.customer.messaginginapp.ModuleMessagingInApp
import io.customer.messaginginapp.type.InAppEventListener
import io.customer.messaginginapp.type.InAppMessage
import io.customer.sdk.data.model.Region

addCustomerIOModule(
  ModuleMessagingInApp(
    MessagingInAppModuleConfig.Builder(
      siteId = <SITE_ID>,
      // If you're in the EU, set Region.EU
      region = Region.US
    )
    // Optional: Set in-app message event listener
    .setEventListener(object : InAppEventListener {
      ...
    })
    .build()
  )
)
```

#### Java

```
import io.customer.messaginginapp.MessagingInAppModuleConfig;
import io.customer.messaginginapp.ModuleMessagingInApp;
import io.customer.messaginginapp.type.InAppEventListener;
import io.customer.messaginginapp.type.InAppMessage;
import io.customer.sdk.data.model.Region;

builder.addCustomerIOModule(
  // If you're in the EU, set Region.EU
  new ModuleMessagingInApp(new MessagingInAppModuleConfig.Builder(<SITE_ID>, Region.US.INSTANCE)
    // Optional: Set in-app message event listener
    .setEventListener(new InAppEventListener() {
      ...
    })
    .build()));
```

For more details and customization options, see [In-App Messaging](../in-app/in-app/).

---

## Getting started > Auth

**Source:** /integrations/sdk/android/getting-started/auth

# Authentication

To use the SDK, you'll need to get two kinds of keys: A CDP *API Key* to send data to Customer.io and a *Site ID*, telling the SDK which workspace your messages come from.

To get your SDK keys and send data to the right places, you’ll need to set up your app as a data in integration in Customer.io, and route it to your workspace. The SDK lets you route data to any number of destinations, but you *must* connect it to your workspace destination to send data, like the profiles you identify, the events you track, and so on, to Customer.io.

**If you haven’t already set up your app as an integration in Customer.io, [do that first](#set-up-a-new-source).**

## API Keys you’ll need

1.  **API Key**: This key, shown in code samples as `cdpApiKey`, lets you send data to Customer.io. You’ll need it to initialize the SDK. You’ll get this key when you set up your mobile app as a data in integration in Customer.io.
2.  **Site ID**: This key tells the SDK which workspace your messages come from. You’ll use it to initialize the `MessagingInApp` package and send in-app messages from your workspace. If you’re upgrading from a previous version of the Customer.io SDK, it also serves as the `migrationSiteId`.

## Get your API Key

You’ll use your write key to initialize the SDK and send data to Customer.io; you’ll get this key from your mobile app’s integration card in Customer.io. If you haven’t already set up your Android integration in Customer.io, you’ll need to [do that first](#set-up-a-new-source).

1.  Go to *Integrations* and click **Add Integration**.
2.  On the *Overview* tab, select your Android integration. If you don’t see your Android integration, you’ll need to [set one up](#set-up-a-new-source).
    
     ![the connections page, showing an android source connected to a journeys destination](https://customer.io/images/cdp-android-connected-destination.png)
    
3.  Go to **Settings** and find your **API Key**. Copy this key into your initialization call. If you’re upgrading from a previous version of the SDK, you should keep the `siteId` that you used in previous versions as the `migrationSiteId` in your config.
    
     ![get your CDP API Key from your source's settings page](https://customer.io/images/cdp-android-source-api-key.png)
    
    ```
    val builder = CustomerIOConfigBuilder(
        applicationContext = this,
        cdpApiKey = "your_cdp_api_key"
    ).addCustomerIOModule(
        ModuleMessagingInApp(
            config = MessagingInAppModuleConfig.Builder(
                siteId = "your_site_id",
                region = Region.US // Replace with Region.EU if your Customer.io account is in the EU.
            ).setEventListener(InAppMessageEventListener()).build()
        )
    ).addCustomerIOModule(ModuleMessagingPushFCM())
    
    CustomerIO.initialize(builder.build())
    ```
    

You're not done yet

You still need your [Site ID](https://fly.customer.io/env/last/settings/api_credentials) to initialize the `MessagingInApp` package and to support people updating your app from a previous version of Customer.io SDK. See [Get your Site ID](#get-your-site-id) below.

### Set up a new integration in Customer.io

If you don’t already have a write key, you’ll need to set up a new data in integration in Customer.io. The “integration” represents your app and the stream of data that you’ll send to Customer.io.

1.  Go to [*Integrations*](https://fly.customer.io/workspaces/last/journeys/integrations/all/overview) and click **Add Integration**.
2.  Select **Android**.
    
     ![set up your android source](https://customer.io/images/cdp-android-source.png)
    
3.  Enter a *Name* for your integration, like “My Android App”.
4.  We’ll present you with a code sample containing a `cdpApiKey` that you’ll use to initialize the SDK. Copy this key and keep it handy.
5.  Click **Complete Setup** to finish setting up your integration.
    
     ![Set your name, get your CDP API Key, and click Complete Setup](https://customer.io/images/cdp-android-source-setup.png)
    

Now the *Integrations* page shows that your Android integration is connected to your workspace. You can also [connect your Android integration to other services](/integrations/data-out/add-destination/) if you want to send your mobile data to other places outside of Customer.io—like your analytics provider, data warehouse, or CRM.

## Get your Site ID

You’ll use your Site ID to initialize the `MessagingInApp` package and send in-app messages from your workspace.

If you’re upgrading from a previous version, my can also set your Site ID as your `migrationSiteId`. This key is used to send remaining tasks to Customer.io when your audience updates your app.

1.  Go to and select **Workspace Settings** in the upper-right corner of the Customer.io app and go to [**API and Webhook Credentials**](https://fly.customer.io/workspaces/last/settings/api_credentials).
    
2.  Copy the **Site ID** for the set of credentials that you want to send your in-app messages from. If you don’t have a set of credentials, click **Create Tracking API Key**.
    
     ![find your site ID](https://customer.io/images/cdp-js-site-id.png)
    
3.  You’ll use this key to initialize the `MessagingInApp` package.
    
    ```
    val builder = CustomerIOConfigBuilder(
        applicationContext = this,
        cdpApiKey = "your_cdp_api_key"
    ).migrationSiteId("your_site_id")
        .addCustomerIOModule(
            ModuleMessagingInApp(
                config = MessagingInAppModuleConfig.Builder(
                    siteId = "your_site_id",
                    region = Region.US // Replace with Region.EU if your Customer.io account is in the EU.
                ).setEventListener(InAppMessageEventListener()).build()
            )
        )
        .addCustomerIOModule(ModuleMessagingPushFCM())
    
    CustomerIO.initialize(builder.build())
    ```
    

## Securing your credentials

To simplify things, code samples in our documentation sometimes show API keys directly in your code. But you don’t have to hard-code your keys in your app. You can use environment variables, management tools that handle secrets, or other methods to keep your keys secure if you’re concerned about security.

To be clear, the keys that you’ll use to initialize the SDK don’t provide read access to data in Customer.io; they only write data to Customer.io. A bad actor who found your credentials can’t use your keys to read data from our servers.

---

## Getting started > How it works

**Source:** /integrations/sdk/android/getting-started/how-it-works

# How it works

Before you can take advantage of our SDK, you need to install the module(s) you want to use, initialize the SDK, and understand the order of operations.

Our SDKs provide a ready-made integration to identify profiles that use mobile devices and send them notifications. Before you start using the SDK, you should understand a bit about how the SDK works with Customer.io.

Before a person logs into your app, any activity they perform is associated with an anonymous person in Customer.io. In this state, you can track their activity, but you can’t send them messages through Customer.io.

When someone logs into your app, you’ll send an `identify` call to Customer.io. This makes the profile eligible to receive messages and reconciles their anonymous activity to their identified profile in Customer.io.

You send messages to a profile through the Customer.io automation builder, broadcasts, etc. These messages are not stored on the device side. If you want to send an event-triggered automation to a mobile device, the mobile device user must be identified and have a connection such that it can send an event back to Customer.io and receive a message payload.

### Your app is a data source and Customer.io is a destination

Our SDK is a data in integration. It routes data from your app to both Customer.io *and* any other outbound services where you might use your mobile data. This makes it easy to use your app as a part of your larger data stack without using extra packages or code.

When you set up your app, you’ll integrate our SDK. But you’ll also determine where you want to route your data to—your Customer.io workspace *and* destinations outside of Customer.io.

## Minimum requirements

To support the Customer.io SDK, you must:

*   Use Gradle 8.0 or later.
*   Use Android Gradle plugin version 8.0 or later (8.2+ recommended).
*   Use Kotlin 1.9.20 or later (2.0+ required if using Kotlin Multiplatform or K2-specific features).
*   Have an Android device or emulator with Google Play Services enabled and a minimum OS version between Android 5.0 (API level 21) and Android 13.0 (API level 33).

## The Processing Queue

The SDK automatically adds all calls to a queue system, and waits to perform these calls until certain criteria is met. This queue makes things easier, both for you and your users: it handles errors and retries for you (even when users lose connectivity), and it can save users’ battery life by batching requests.

The queue holds requests until any one of the following criteria is met:

*   There are 20 or more tasks in the queue.
*   30 seconds have passed since the SDK performed its last task.
*   The app is closed and re-opened.

For example, when you identify a new profile in your app using the SDK, you won’t see the created/updated profile immediately. You’ll have to wait for the SDK to meet any of the criteria above before the SDK sends a request to the Customer.io API. Then, if the request is successful, you’ll see your created/updated profile in your workspace.

---

## Getting started > Packages options

**Source:** /integrations/sdk/android/getting-started/packages-options

# Packages and configuration options

The SDK consists of a few packages. You'll get the most value out of Customer.io when you use all our packages together, but this lets you omit packages for features you don't intend to use.

## SDK packages

To minimize our SDK’s impact on your app’s size, we’ve split the SDK into packages. You can limit your install to the packages that you need for your project. But, in most cases, you’ll want to install all the packages to get the most value out of Customer.io.

You must install the `datapipelines` package. It lets you identify profiles, which you must do before you can send them messages, etc. You’ll want to add the `messaging-push-fcm` and the `messaging-in-app` packages to send push notifications and in-app messages respectively.

Package Product

Required?

Description

datapipelines

:white\_check\_mark:

Identify profiles, track events, track anonymous activity

messaging-push-fcm

Receive push notifications over Google Firebase Cloud Messaging (FCM)

messaging-in-app

Receive in-app notifications

location

[Enrich user profiles with accurate device location](/integrations/sdk/android/tracking/location/)

geofence

[Monitor geofences and send location-based events](/integrations/sdk/android/tracking/geofencing/). Includes the `location` package automatically

## Configuration options

You’ll call configuration options before you initialize the SDK. In most cases, you’ll want to stick with the defaults, but you might do things like change the `logLevel` when testing updates to your app—as shown in the example below.

If you’re in our EU region, you must set `Region.EU`.

```
val builder = CustomerIOConfigBuilder(
    applicationContext = this,
    cdpApiKey = "your_cdp_api_key"
).region(Region.US)
    .autoTrackDeviceAttributes(true)
    .autoTrackActivityScreens(false)
    .screenViewUse(ScreenView.All)
    .logLevel(CioLogLevel.DEBUG)
    .addCustomerIOModule(
        ModuleMessagingInApp(
            config = MessagingInAppModuleConfig.Builder(
                siteId = "your_site_id",
                region = Region.US
            ).setEventListener(InAppMessageEventListener()).build()
        )
    )
    .addCustomerIOModule(ModuleMessagingPushFCM())

CustomerIO.initialize(builder.build())
```

Option

Type

Default

Description

`cdpApiKey`

string

**Required**: the key you’ll use to initialize the SDK and send data to Customer.io

`region`

`Region.EU` or `Region.US`

`Region.US`

Because we default to the US region, **you must set this to `Region.EU` if your account is in the EU region.**

`apiHost`

string

The domain you’ll proxy requests through. You’ll only need to set this (and `cdnHost`) if you’re [proxying requests](#proxying-requests).

`autoTrackDeviceAttributes`

boolean

`true`

Automatically gathers information about devices, like operating system, device locale, model, app version, etc

`autoTrackActivityScreens`

boolean

`false`

If true, the SDK automatically sends `screen` events for every screen your audience visits. **If you use Jetpack Compose** you should set this to false and [track screens manually](/integrations/sdk/android/tracking/screen-events/#manual-screenview).

`cdnHost`

string

The domain you’ll fetch configuration settings from. You’ll only need to set this (and `apiHost`) if you’re [proxying requests](#proxying-requests).

`logLevel`

string

`error`

Sets the level of logs you can view from the SDK. Set to `debug` or `info` to see more logging output.

`migrationSiteId`

string

**Required if you’re updating from 2.x**: the credential for previous versions of the SDK. This key is used to send remaining tasks to Customer.io when your audience updates your app.

`screenViewUse`

`All` or `InApp`

`All`

`ScreenView.All` (Default): Screen events are sent to Customer.io. You can use these events to build segments, trigger automations, and target in-app messages.  
  
`ScreenView.InApp`: Screen view events not sent to Customer.io. You’ll *only* use them to target in-app messages based on page rules.

`trackApplicationLifecycleEvents`

boolean

`true`

Set to `false` if you don’t want the app to send lifecycle events

## Proxying requests

By default, requests go through our domain at `cdp.customer.io`. You can proxy requests through your own domain to provide a better privacy and security story, especially when submitting your app to app stores.

To proxy requests, you’ll need to set the `apiHost` and `cdnHost` properties in your `SDKConfigBuilder`. While these are separate settings, you should set them to the same URL.

While you need to initialize the SDK with a `cdpApiKey`, you can set this to any value you want. You only need to pass your actual key when you send requests from your server backend to Customer.io. If you want to secure requests to your proxy server, you can set the `cdpApiKey` to a value representing basic authentication credentials that you handle on your own. See [proxying requests](/integrations/data-in/proxying-requests/) for more information.

```
val builder = CustomerIOConfigBuilder(
    applicationContext = this,
    cdpApiKey = "your_cdp_api_key"
)
    .region(Region.US)  // Optional but recommended
    .apiHost("your-proxy.example.com")
    .cdnHost("your-proxy.example.com")
    .addCustomerIOModule(ModuleMessagingPushFCM())

CustomerIO.initialize(builder.build())
```

---

## Getting started > Troubleshooting

**Source:** /integrations/sdk/android/getting-started/troubleshooting

# Troubleshooting

If you're having trouble with the SDK, here are some basic steps to troubleshoot your problems, and solutions to some known issues.

## Basic troubleshooting steps

1.  **Update to the latest version**: When troubleshooting problems with our SDKs, we generally recommend that you try updating to the latest version. That helps us weed out issues that might have been seen in previous versions of the SDK.
    
2.  **Try running our MCP server**: Our MCP server includes an `integration` tool that can provide immediate help with troubleshooting your implementation, including problems with push and in-app notifications. See [Use our MCP server to troubleshoot your implementation](#troubleshoot-with-mcp) below.
    
3.  **Enable `debug` logging**: Reproducing your issue with `loglevel(CiologLevel.ERROR)`. This can help you (or us) pinpoint problems.
    
    Don't use debug logging in your production app
    
    Debug mode is great for helping you find problems as you integrate with Customer.io, but we strongly recommend that you don’t set the `loglevel` parameter (which defaults to `CiologLevel.ERROR`) in your publicly available, production app.
    
4.  **Try our test image**: Using [an image](#image-display-issues) that we know works in push and in-app notifications can help you narrow down problems relating to images in your messages.
    

### Troubleshooting issues with our MCP server

Our [MCP server](/ai/mcp/get-started/) includes an `integration` tool that can help troubleshoot your implementation, including problems with push and in-app notifications. It has a deep understanding of our SDKs and provides an immediate way to get support with your implementation—without necessarily needing to capture debug logs, etc.

You can ask the MCP server basic questions like, “My push notifications aren’t working. Can you help me troubleshoot the problem?”

Or you can ask more specific questions like, “Deep links in push notifications don’t work for customers in my Android app.” Or “I’m not receiving metrics for push notifications for iOS users.”

The tool will return detailed steps to help you find and troubleshoot problems.

### Examine calls in the integrations tab

Under *Integrations*, we’ll show you the calls that come in from your SDK and how we interpret those calls for each destination. You can use this information to pinpoiunt problems in your integration.

If you have a problem, you may want to go to *Integrations* and check:

1.  That your Android integration is connected to your Customer.io workspace. If you don’t connect your integration to your workspace, you won’t be able to send messages, etc.
2.  Your integration’s *Data In* tab to make sure that your app sends the right data.
3.  The *Data Out* tab for any data out integrations to make sure that you’re sending the right data from your SDK to the destination. **This includes Customer.io**: your workspace is one of the places you’ll send data from your app!

Check out our [Integrations troubleshooting page](/integrations/getting-started/troubleshooting/) for more help pinpointing issues in your integration.

### If you need to contact support

We’re here to help! If you contact us for help with an SDK-related issue, we’ll generally ask for the following information. Having it ready for us can help us solve your problem faster.

1.  **Share information about your device and environment**: Let us know where you had an issue—the SDK and version of the SDK that you’re using, the specific device, operating system, message, use case, and so on. The more information you share with us, the easier it is for us to weed out externalities and find a solution.
2.  **Share your push or in-app payload**: Knowing what images you used, the shape of your payload, and so on helps us reproduce the issue and figure out exactly what went wrong.
3.  **[Grant access to your workspace](/accounts/security/privacy/#support-team-access)**: It may help us to see exactly what triggers an automation, what data is associated with devices you’re troubleshooting, etc. You can grant access for a limited time, and revoke access at any time.

### Capture logs

Logs help us pinpoint the problem and find a solution. To capture logs from the Customer.io SDK:

1.  **Enable debug logging in your app**.
    
    Note
    
    You should not use debug mode in your production app. Remember to disable debug logging before you release your app to the App Store.
    
    ```
    val builder = CustomerIOConfigBuilder(
        applicationContext = this,
        cdpApiKey = "your_cdp_api_key"
    ).logLevel(CioLogLevel.DEBUG)
    
    CustomerIO.initialize(builder.build())
    ```
    
2.  In *Android Studio*, build and run your app on a physical device or emulator.
    
3.  Select *View > Tool Windows > Logcat*. This shows you your device’s logs.
    
4.  Filter for `CIO` in the top to find log messages specific to the Customer.io SDK.
    
5.  Save your log and send it to our Support team at [win@customer.io](mailto:win@customer.io). In your message, describe your problem and provide relevant information about:
    
    *   The version of the SDK you’re using.
    *   The type of problem you’ve encountered.
    *   An existing GitHub issue URL or existing support email so we know what these log files are in reference to.

## NaN, infinite, or imaginary number values

Customer.io doesn’t handle invalid JSON values in your payloads, like `NaN`, infinite, or imaginary number values. If you send these values in `identify`, `track`, `screen`, or similar calls, we’ll drop them and record errors.

While we drop invalid values, we don’t drop the entire payload. The operation itself will still succeed. For example, if you send an identify call with two attributes, one of which is a `NaN` value, we’ll drop the `NaN` value, but the identify call succeeds with the other attribute.

## Image display issues

If you’re having trouble, try using our test image in a message! If it works, then there’s likely a problem with your original image.

 ![a test image of a bird that we know will work with all push notifications](https://customer.io/images/push-image-control.jpg)

Android and iOS devices support different image sizes and formats. In general, you should stick to the smallest size (under 1 MB—the limit for Android devices) and common formats (PNG, JPEG).

iOS

Android

In-App (all platforms)

**Format**

JPEG, PNG, BMP, GIF

JPEG, PNG, BMP

JPEG, PNG, GIF

**Maximum size**

10 MB\*

1 MB

**Maximum resolution**

2048 x 1024 px

1038 x 1038 px

For linked media only. If you host images in our Asset Library, you’re limited to 3MB per image.

## Push notification issues

### Why didn’t everybody in my segment get a push notification?

If your [segment](/journeys/segmentation/data-driven-segments/) doesn’t specify profiles that have an existing device, it’s likely that profiles entered your segment without using your app. If you send a push notification to such a segment, the “Sent” count will probably show fewer sends than there were profiles in your segment.

### Why are messages sent but not delivered or opened?

The *sent* status means that we sent a message to your delivery provider—APNS or FCM. It’ll be marked *delivered* or *opened* when the delivery provider forwards the message to the device and the SDK reports the metric back to Customer.io. If a profile turned their device off or put it in airplane mode, they won’t receive your push notification until they’re back on a network.

### FCM `SENDER_ID_MISMATCH` error

This error occurs when the FCM Sender ID in your app does not match the Sender ID in your Firebase project. To resolve this issue, you’ll need to ensure that the Sender ID in your app matches the Sender ID in your Firebase project.

1.  Check that you uploaded the correct JSON certificate to Customer.io. If your JSON certificate represents the wrong Firebase project, you may see this error.
2.  Verify that the Sender ID in your app matches the Sender ID in your Firebase project.
3.  If you imported devices (device tokens) from a previous project, make sure that you imported tokens from the correct Firebase project. If the tokens represent a different app than the one you send push notifications to, you’ll see this error.

## In-App message issues

### My in-app messages are sent but not delivered

Profiles won’t get your message until they open your app. If you use page rules, they won’t see your message until they visit the right screen(s), so delivery times for in-app messages can vary significantly from other types of messages.

---

## In app > In app

**Source:** /integrations/sdk/android/in-app/in-app

# In-app messages

Incorporate in-app messages to send dynamic, personalized content to people using your app. With in-app messages, you can speak directly to your app's users when they use your app.

## How it works

An in-app message is a message that people see within the app. To set up in app messaging, install and initialize the `tracking` and `messaging-in-app` packages.

People won’t see your in-app messages until they open your app. If you set an *expiry* period for your message, and that time elapses before someone opens your app, they won’t see your message.

You can also set *page rules* to display your in-app messages when people visit specific pages in your app. However, to take advantage of page rules, you need to implement screen tracking features. Screen tracking tells us the names of your pages and which page a person is on, so we can display in-app messages on the correct pages in your app.

## Install the SDK and in-app module

1.  Make sure that you add update your repositories in the `settings.gradle` file to include the in-app SDK.
    
    ```
    dependencyResolutionManagement {
       repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
       repositories {
          google()
          mavenCentral()
          // only needed for in-app messaging in SDK versions below 3.6.1
          // maven { url 'https://maven.gist.build' }
       }
    }
    ```
    
2.  Implement the `messaging-in-app` package.
    
    ```
    implementation "io.customer.android:messaging-in-app:4.20.3"
    ```
    

## Initialize the SDK with the in-app module

Simply initialize the SDK with the `MessagingInApp` module and your app will be able to receive in-app messages. Create an automation and send your first in-app message to test your implementation!

```
val builder = CustomerIOConfigBuilder(
    applicationContext = this,
    cdpApiKey = "your-cdp-api-key"
).addCustomerIOModule(ModuleMessagingPushFCM())
    .addCustomerIOModule(
        ModuleMessagingInApp(
            config = MessagingInAppModuleConfig.Builder(siteId = "site-id", region = Region.US)
                .setEventListener(object : InAppEventListener {
                    override fun errorWithMessage(message: InAppMessage) {}
                    override fun messageActionTaken(message: InAppMessage, actionValue: String, actionName: String) {}
                    override fun messageDismissed(message: InAppMessage) {}
                    override fun messageShown(message: InAppMessage) {}
                })
                .build()
        )
    )
    .logLevel(CioLogLevel.DEBUG)
    // For fragment-based apps or Jetpack Compose, disable this and use manual screen tracking
    .autoTrackActivityScreens(true)
    .region(Region.US)

CustomerIO.initialize(builder.build())
```

## Set the color scheme

New in v4.19.0

In-app messages follow the device’s light or dark-mode appearance by default. When someone uses dark mode, messages render with the [dark mode values](/messaging/channels/in-app/global-styles-in-app/#set-light-and-dark-mode-styles) you set for your in-app messages in Design Studio. Where a message has no dark value, it keeps its light-mode styles in both modes.

You can set the color scheme on the `MessagingInApp` module config to override this behavior.

Value

Description

`ColorScheme.AUTO`

**(Default)** Follow the device appearance and switch automatically when the device does.

`ColorScheme.LIGHT`

Always render light styles.

`ColorScheme.DARK`

Always render dark styles.

```
import io.customer.messaginginapp.type.ColorScheme

ModuleMessagingInApp(
    config = MessagingInAppModuleConfig.Builder(siteId = "site-id", region = Region.US)
        .setColorScheme(ColorScheme.DARK)
        .build()
)
```

You can also change the color scheme at runtime. The new value applies to messages already on screen and persists for the rest of the session.

```
ModuleMessagingInApp.instance().setColorScheme(ColorScheme.DARK)
```

## Page rules

You can set page rules when you create an in-app message. A page rule determines the page that your audience must visit in your app to see your message. However, before you can take advantage of page rules, you need to:

1.  Enable screen tracking in the SDK. If you are using a view(xml)-based() UI with activities, you can enable automatic screen tracking by calling `autoTrackActivityScreens(true)` during SDK initialization. However, if your app is fragment-based or utilizes Jetpack Compose, we recommend utilizing manual screen tracking with the `screen` method.
2.  Provide page names to whomever sets up in-app messages in fly.customer.io.

The SDK automatically uses label in your manifest file as the page/screen name. If your screens don’t have labels, you won’t be able to set up page rules and screenview events will have an empty event `name`.

You should inform anybody creating in-app messages about page names if you want to set up page rules, to make sure that your messages appear on the right pages of your app. If we don’t recognize the page that you set for a page rule, your audience will never see your message.

 ![Set up page rules to limit in app messages by page](https://customer.io/images/in-app-page-rule.png)

Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the `name` in your `screen` events. If you’re targeting your website, your page rules should always be lowercase.

 ![The first page rule is Web contains /dashboard. The second page rule is iOS contains Dashboard.](https://customer.io/images/page-rule-case-sensitive.png)

## Anonymous messages

As of version 4.12, you can send [anonymous in-app messages](/journeys/anonymous-in-app/). These are messages that are sent *only* to people you haven't identified yet.

You *can* use lead forms in anonymous messages to capture leads and potentially identify people when they submit your form. For example, you could use a lead form and offer a coupon or newsletter to people who provide their email addresses. See [Lead forms](/journeys/messages-and-webhooks/in-app/lead-form/) for more information.

---

## In app > In app event listeners

**Source:** /integrations/sdk/android/in-app/in-app-event-listeners

# In-app event listeners

When people receive an in-app message, you'll listen for events to handle the message's lifecycle—like dismissing the event or taking a custom action.

## Handle responses to messages (event listeners)

You can set up event listeners to handle your audience’s response to your messages. For example, you might run different code in your app when your audience taps a button in your message or when they dismiss the message without tapping a button.

You can listen for four different events:

*   `messageShown`: a message is “sent” and appears to a user
*   `messageDismissed`: the user closes the message (by tapping an element that uses the `close` action)
*   `errorWithMessage`: the message itself produces an error—this probably prevents the message from appearing to the user
*   `messageActionTaken`: the user performs an action in the message.

```
val builder = CustomerIOConfigBuilder(
    applicationContext = this,
    cdpApiKey = "your-cdp-api-key"
).addCustomerIOModule(
    ModuleMessagingInApp(config = MessagingInAppModuleConfig.Builder(siteId = "site-id", region = Region.US)
        .setEventListener(object : InAppEventListener {
            override fun messageShown(message: InAppMessage) {
                trackInAppEvent("messageShown", message)
            }

            override fun messageDismissed(message: InAppMessage) {
                trackInAppEvent("messageDismissed", message)
            }

            override fun errorWithMessage(message: InAppMessage) {
                trackInAppEvent("errorWithMessage", message)
            }

            override fun messageActionTaken(
                message: InAppMessage,
                actionValue: String,
                actionName: String
            ) {
                trackInAppEvent(
                    "messageActionTaken",
                    message,
                    hashMapOf("action-value" to actionValue, "action-name" to actionName)
                )
            }
        })
        .build()
    )
)

CustomerIO.initialize(builder.build())

private fun trackInAppEvent(
   eventName: String,
   message: InAppMessage,
   arguments: Map<String, String>? = null
) {
   CustomerIO.instance().track(
      "in-app message action",
      HashMap<String, String>().apply {
         arguments?.let { putAll(it) }
         put("event-name", eventName)
         put("message-id", message.messageId)
         put("delivery-id", message.deliveryId ?: "NULL")
      }
   )
}
```

### Handling custom actions

When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting.

In these cases, you’ll want to use the `messageActionTaken` event listener and listen for custom action names or values to execute code. While you’ll have to write custom code to handle custom actions, the SDK helps you [listen for in-app message events](#event-listeners) including your custom action, so you know when to execute your custom code.

1.  When you [add an action to an in-app message](/messaging/channels/in-app/send-in-app-message/) in Customer.io, select *Custom Action* and set your Action’s *Name* and value. The *Name* corresponds to the `actionName`, and the value represents the `actionValue` in your event listener.
    
     ![Set up a custom in-app action](https://customer.io/images/in-app-custom-action.png)
    
2.  [Register an event listener](#event-listener) for `MessageActionTaken`, and listen for the `actionName` or `actionValue` you set up in the previous step.
    
    Use names and values exactly as entered
    
    We don’t modify your action’s name or value, so you’ll need to match the case of names or values exactly as entered in your *Custom Action*.
    
3.  When someone receives a message and invokes the action (tapping a button, tapping a message, etc), your app will perform the custom action.
    

### Dismiss in-app message

You can dismiss the currently display in-app message with the following method. This can be particularly useful to dismiss in-app messages when your audience clicks or taps custom actions.

```
    CustomerIO.instance().inAppMessaging().dismissMessage()
```

---

## In app > Inbox

**Source:** /integrations/sdk/android/in-app/inbox

# Notification inbox

## How it works

Unlike other messages, inbox messages don’t necessarily appear immediately to your audience, and they don’t disappear when a person dismisses them. Instead, messages collect in a notification inbox that people can check at their leisure. You send inbox messages as part of an [automation, broadcast](/messaging/channels/in-app/inbox/send-inbox/), or [transactional message](/messaging/channels/in-app/inbox/send-inbox-txnl/).

The SDK gives you two ways to show the inbox in your app:

*   **Recommended: [add the visual inbox](#add-the-visual-inbox).** Mount the SDK’s ready-made inbox views—a bell icon with an unread badge and a panel that lists messages. The SDK renders the [inbox you style and publish](/messaging/channels/in-app/inbox/get-started/) in Customer.io using native UI—no webviews—and handles fetching messages, unread counts, opens, and clicks for you.
*   **Advanced: [build your own inbox](#build-your-own-inbox).** The SDK delivers inbox messages as JSON payloads, and you render them in your own inbox UI. Take this path when you need complete control over the inbox experience.

## Add the visual inbox

The visual inbox renders the same inbox you publish for your website: your icon, colors, position, unread indicator, and message templates—including dark mode styles. When there’s nothing to show—the inbox isn’t published or has no messages—the inbox views render nothing, so they’re safe to mount unconditionally.

### Prerequisites

*   [Create, style, and publish a notification inbox](/messaging/channels/in-app/inbox/get-started/) in your workspace.
*   [Set up in-app messaging](/integrations/sdk/android/in-app/in-app/) in your app. The inbox module builds on the in-app messaging module.
*   The visual inbox views are Jetpack Compose composables. If your app uses XML layouts, you can host them in a `ComposeView`.

### Install the inbox module

The visual inbox ships as its own module, so apps that don’t use it don’t carry its dependencies. Add the dependency to your app’s `build.gradle`.

```
implementation "io.customer.android:messaging-inbox:4.20.3"
```

### Add the inbox to your app

`NotificationInboxOverlay` is the all-in-one option: a floating bell button with an unread badge that slides out the message list, and a scrim that closes it when a person taps outside. Mount it once, near the top of your Compose hierarchy, so it overlays the rest of your content.

```
import io.customer.messaginginbox.NotificationInboxOverlay

Box(modifier = Modifier.fillMaxSize()) {
    AppContent()
    NotificationInboxOverlay()
}
```

That’s the whole integration. The overlay fetches messages, shows and updates the unread badge, marks messages opened when the panel opens, and reports opens and clicks to your [inbox metrics](/messaging/channels/in-app/inbox/metrics/).

If your app uses XML layouts instead of Compose, host the overlay in a [`ComposeView` ↗](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/compose-in-views) that sits on top of your layout:

```
composeView.setContent {
    NotificationInboxOverlay()
}
```

### Place the bell and message list yourself

If the floating overlay doesn’t fit your design, compose the two underlying views directly:

*   `NotificationInboxBell`: the bell button with the unread badge. Place it anywhere—a top app bar, a tab, a corner—and use its `onClick` to present your inbox.
*   `NotificationInboxView`: the message list without any surrounding chrome. Embed it in a bottom sheet, a dialog, or a dedicated inbox screen.

```
import io.customer.messaginginbox.NotificationInboxBell
import io.customer.messaginginbox.NotificationInboxView

var isInboxVisible by remember { mutableStateOf(false) }

Scaffold(
    topBar = {
        TopAppBar(
            title = { Text("Dashboard") },
            actions = {
                NotificationInboxBell(onClick = { isInboxVisible = true })
            }
        )
    }
) { padding ->
    AppContent(Modifier.padding(padding))

    if (isInboxVisible) {
        ModalBottomSheet(onDismissRequest = { isInboxVisible = false }) {
            NotificationInboxView(modifier = Modifier.fillMaxWidth())
        }
    }
}
```

### Handle message actions

When a person taps a message or a button inside it, the SDK runs the action you set on the message: it opens URLs in the browser, removes the message for dismiss actions, and so on. If your messages use deep links or you want to route navigation through your own code, set an `InboxEventListener` when you configure the in-app messaging module.

```
import io.customer.messaginginapp.MessagingInAppModuleConfig
import io.customer.messaginginapp.ModuleMessagingInApp
import io.customer.messaginginapp.type.InboxEventListener

ModuleMessagingInApp(
    config = MessagingInAppModuleConfig.Builder(siteId = "site-id", region = Region.US)
        .setInboxEventListener(object : InboxEventListener {
            override fun messageActionTaken(
                message: InboxMessage,
                actionName: String,
                actionValue: String
            ): Boolean {
                if (actionValue.startsWith("myapp://")) {
                    // Navigate within your app
                    deepLinkRouter.open(actionValue)
                    // Return true to tell the SDK you handled the action,
                    // so it doesn't run its default behavior
                    return true
                }
                // Return false to let the SDK handle the action
                return false
            }
        })
        .build()
)
```

You can also set or replace the listener at runtime:

```
ModuleMessagingInApp.instance().setInboxEventListener(listener)
```

The listener also receives optional, observe-only callbacks you can use for analytics or app state—their return values don’t change SDK behavior:

Callback

When it fires

`messageActionTaken(message, actionName, actionValue)`

A person taps a non-dismiss action. Return `true` to handle the action yourself, or `false` to let the SDK handle it.

`messageShown(message)`

A message renders in the visible inbox for the first time.

`messageOpened(message)`

The SDK marks a message opened—for example, when the panel opens.

`messageDismissed(message)`

A person dismisses a message, removing it from the inbox.

Dismiss actions always remove the message; they aren’t routed to your listener.

### Visual inbox behaviors

*   **The bell hides itself when there’s nothing to show.** If your inbox isn’t published, or the person has no messages, the bell and panel don’t render at all.
*   **Opening the inbox marks messages opened.** Messages visible in the open panel—or in an embedded `NotificationInboxView`—are marked opened automatically, and the unread badge updates to match.
*   **Dismissing a message deletes it.** When a person dismisses a message, the SDK removes it from their inbox on every device.
*   **Dark mode is automatic.** The inbox follows the device’s appearance and re-renders when it changes, using the dark mode colors you set in Design Studio.
*   **Style changes flow to your app.** When you update your inbox’s styles or templates in Customer.io, the SDK picks up the changes on its next refresh—no app update required.

## Build your own inbox

If you need complete control over the inbox experience, skip the visual inbox and work with the message payloads directly. Customer.io delivers inbox messages as JSON payloads, not fully rendered messages. The SDK helps you listen for these payloads, but you determine how to display them in your own inbox client.

## Get the inbox instance

You’ll access inbox functionality through the `inbox()` method on the in-app messaging module.

```
val inbox = CustomerIO.instance().inAppMessaging().inbox()
```

## Inbox methods

The inbox instance provides several methods to manage messages.

Method

Description

`getMessages(topic?)`

Suspend function to get messages from the inbox. Optionally filter by topic. Returns a list of messages.

`fetchMessages(callback)`

Fetch messages from the inbox with a callback. Returns a result with messages on success or error on failure.

`fetchMessages(topic, callback)`

Fetch messages filtered by topic with a callback. Returns a result with messages on success or error on failure.

`addChangeListener(listener)`

Add a listener to be notified when messages change.

`removeChangeListener(listener)`

Remove a previously added change listener.

`markMessageOpened(message)`

Mark a message as opened.

`markMessageUnopened(message)`

Mark a message as unopened.

`markMessageDeleted(message)`

Mark a message as deleted.

`trackMessageClicked(message, actionName?)`

Track a click on the message. The `actionName` parameter is optional.

## Inbox message payloads

Inbox messages are delivered as a JSON payload. The SDK helps you listen for the payload, but you’ll render the content in your own inbox client.

The client payload includes the following fields, but you’re most concerned with the `properties` object, which represents your message content. By default, we’ll send a `title` and `body` field, but you can add other fields like an `image` or a `link`—whatever you set up your inbox to expect.

Make sure that your team members know what payloads to send—especially if you expect different payloads for different topics or types of messages.

Field

Type

Description

`messageId`

string

Unique identifier for the message.

`sentAt`

string

When the message was sent.

`expiresAt`

string

When the message will expire.

`opened`

boolean

Whether the message has been opened.

`topics`

array

The topics that the message belongs to.

`type`

string

The type of message.

`properties`

object

The properties of the message.

```
{
    "messageId": "1234567890",
    "sentAt": "2026-02-05T12:00:00Z",
    "expiresAt": "2026-02-05T12:00:00Z",
    "opened": false,
    "topics": ["orders", "shipping"],
    "type": "order_shipped",
    "properties": {
        "title": "Hey Cool Person, your order shipped!",
        "body": "You can track your order #1234567890 here:",
        "link": "https://example.com/orders/1234567890"
    }
}
```

### Inbox topics and types

When you send an inbox message, you can assign it to one or more topics. You can use these topics to filter messages when you fetch them. You can also use the topics to determine how to render the messages in your notification inbox.

Messages also have a `type`. Think of this like a sub-category or topic for a message. For example, you might have `orders` and `sale` topics, where `orders` don’t have images but `sale` topics might. Or, within the `orders` topic, you might have `order_placed` and `order_shipped` types, where `order_placed` lists order details and images of purchased products and `order_shipped` provides a link to the tracking information for the order that opens in a new tab.

## Setup your notification inbox

Inbox messages are just JSON payloads. You’ll need to build your own inbox client to display the messages. The code below gives you a starting point, but you can build your own inbox client however you want.

### Fetch messages

```
// Using suspend function (recommended)
val messages = notificationInbox.getMessages()

// Fetch messages filtered by topic
val promoMessages = notificationInbox.getMessages(topic = "promotions")

// Using callback
notificationInbox.fetchMessages { result ->
    runOnUiThread {
        result.onSuccess { messages ->
            // Update your UI with the messages
            updateInboxUI(messages)
        }.onFailure { error ->
            // Handle error
            Log.e("Inbox", "Failed to fetch messages", error)
        }
    }
}

// Using callback with topic filter
notificationInbox.fetchMessages("promotions") { result ->
    runOnUiThread {
        result.onSuccess { messages ->
            // Update your UI with filtered messages
            updatePromotionsUI(messages)
        }.onFailure { error ->
            Log.e("Inbox", "Failed to fetch messages", error)
        }
    }
}
```

### Listen for message updates

```
// Create a change listener
val notificationInboxChangeListener = object : NotificationInboxChangeListener {
    override fun onMessagesChanged(messages: List<InboxMessage>) {
        // Update your UI with the new messages
        updateInboxUI(messages)
    }
}

// Add the listener
notificationInbox.addChangeListener(notificationInboxChangeListener)

// Don't forget to remove the listener when you're done
notificationInbox.removeChangeListener(notificationInboxChangeListener)
```

### Mark messages as opened or unopened

```
// Mark a message as opened
notificationInbox.markMessageOpened(message)

// Mark a message as unopened
notificationInbox.markMessageUnopened(message)
```

### Track message clicks

```
// Track a click without an action name
notificationInbox.trackMessageClicked(message)

// Track a click with an action name
notificationInbox.trackMessageClicked(message, "view_order")
```

### Delete messages

```
// Mark a message as deleted
notificationInbox.markMessageDeleted(message)
```

## Working with message properties

You can access message properties to display custom content in your inbox:

```
// Access message properties
val title = message.properties["title"] as? String
val body = message.properties["body"] as? String
val link = message.properties["link"] as? String
val imageUrl = message.properties["image"] as? String

// Handle message action when user taps
fun handleMessageTap(message: InboxMessage) {
    // Mark as opened
    notificationInbox.markMessageOpened(message)

    // Track the click
    notificationInbox.trackMessageClicked(message)

    // Open link if available
    val link = message.properties["link"] as? String
    if (link != null) {
        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
        startActivity(intent)
    }
}
```

---

## In app > Inline in app

**Source:** /integrations/sdk/android/in-app/inline-in-app

# Inline in-app messages

Inline in-app messages help you send dynamic content into your app. The messages can look and feel like a part of your app, but provide fresh and timely content without requiring app updates.

## How it works

An inline message targets a specific view in your app. Basically, you’ll create an empty placeholder view in your app’s UI, and we’ll fill it with the content of your message.

This makes it easy to show dynamic content in your app without development effort. You don’t need to force an update every time you want to talk to your audience. *And*, unlike push notifications, banners, toasts, and so on, in-line messages can look like natural parts of your app.

## Install dependencies

Add the appropriate dependency to your `build.gradle` file based on your UI framework:

```
dependencies {
  // For Android XML views
  implementation "io.customer.android:messaging-in-app:4.20.3"
  
  // For Jetpack Compose (includes messaging-in-app internally)
  implementation "io.customer.android:messaging-in-app-compose:4.20.3"
}
```

Note

If you’re using **Jetpack Compose**, you only need the `messaging-in-app-compose` dependency. It includes the standard in-app messaging functionality internally.

Tip

For more information about installing and configuring the Customer.io SDK, see our [Quick Start Guide](../../quick-start-guide/#install).

## 1\. Add View to your app UI to support inline messages

You’ll need to include a UI element in your app UI to render inline messages. Avoid setting a fixed height on this view as its height will automatically adjust when messages are loaded or interacted with.

Tip

We’ve set up examples in [our sample apps ↗](https://github.com/customerio/customerio-android/tree/main/samples/) that might help if you want to see a real-world implementation of this feature.

Android XMLJetpack Compose

#### Android XML

1.  Open your layout XML file and add `InlineInAppMessageView` to your layout as shown in the example below.
    
2.  Set up layout constraints: you’re responsible for setting the width and the leading, top, trailing, and bottom constraints for the view. See [view layout](#view-layout) for more information.
    
3.  Set the `elementId` in your XML or programmatically in your activity/fragment. This ID is used in Customer.io UI to target this view when sending an in-app message.
    
    ```
     <io.customer.messaginginapp.ui.InlineInAppMessageView
         android:id="@+id/example_in_app_message"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         app:elementId=<example-element-id> />
    ```
    
4.  Or you can set the `elementId` directly in code as shown below:
    
    ```
    val inlineView = findViewById<InlineInAppMessageView>(R.id.example_in_app_message)
    inlineView.elementId = <example-element-id>
    ```
    

#### Jetpack Compose

1.  Add the `InlineInAppMessage` to your UI as shown in the example below.
    
2.  Set layout modifiers to position the view. Avoid setting a fixed height, `InlineInAppMessage` will adjust its height automatically when messages load or are interacted with. See [view layout](#view-layout) for more information.
    
3.  Set the `elementId` in your composable. This ID is used in Customer.io UI to target this view when sending an in-app message.
    
    ```
    import io.customer.messaginginapp.compose.InlineInAppMessage
    
    InlineInAppMessage(
        elementId = <example-element-id>,
        modifier = Modifier.fillMaxWidth()
    )
    ```
    

### Set up layout constraints for your message

Inline message views automatically adjust their height at runtime when messages load or users interact with them. Use `wrap_content` for the view’s height. This lets the view resize itself dynamically.

You’re responsible for setting layout constraints or modifiers to position your view correctly (like start, top, or end). You shouldn’t use a fixed height, as it might interfere with message rendering. If you’re using XML, Android Studio might show warnings if you don’t set a height. The `wrap_content` setting satisfies these warnings without breaking functionality.

## 2\. Build and send your message

When you add an in-app message to a broadcast or automation in Customer.io:

1.  Set the **Display** to **Inline** and set the **Element ID** to the ID you set in your app. If the editor says that the inline display feature is *Web/iOS only*, don’t worry about that. We’re working on updating this UI.
2.  (Optional) If you send multiple messages to the same Element ID, you’ll also want to set the **Priority**. This determines which message we’ll show to your audience first, if there are multiple messages in the queue.

Then craft and send your message!

 ![in-app message settings with the To field set to ID and the display setting set to inline](https://customer.io/images/in-app-inline.png)

## Handling custom actions

When you set up an in-app message, you can determine the “action” to take when someone taps a button, taps your message, etc. In most cases, you’ll want to deep link to a screen, etc. But, in some cases, you might want to execute some custom action or code—like requesting that a user opts into push notifications or enables a particular setting.

While you’ll have to write custom code to handle custom actions, the SDK helps you listen for in-app message events including your custom action, so you know when to execute your custom code.

Follow these steps to implement custom action buttons for inline messages:

### 1\. Compose an in-app message with a custom action

When you [add an action to an in-app message](/messaging/channels/in-app/send-in-app-message/) in Customer.io, select *Custom Action* and set your Action’s *Name* and value. The *Name* corresponds to the `actionName`, and the value represents the `actionValue` in your event listener.

 ![Set up a custom in-app action](https://customer.io/images/in-app-custom-action.png)

### 2\. Listen for events

There are two ways to listen for these click events in inline in-app messages.

1.  Register a delegate with your inline view:
    
    Android XMLJetpack Compose
    
    #### Android XML
    
    ```
    import io.customer.messaginginapp.type.InAppMessage
    import io.customer.messaginginapp.type.InlineMessageActionListener
    import io.customer.messaginginapp.ui.InlineInAppMessageView
    
    class InlineExamplesActivity: AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
         
            val inlineView = findViewById<InlineInAppMessageView>(R.id.example_in_app_message)
            inlineView.setActionListener(MyInlineMessageActionListener())
        }
    }
    
    class MyInlineMessageActionListener : InlineMessageActionListener {
        override fun onActionClick(message: InAppMessage, actionValue: String, actionName: String) {
            // Perform some logic when people tap an action button.
    
            // Example code handling button tap:
            when (actionValue) { // use actionValue or actionName, depending on how you composed the in-app message.
                "enable-auto-renew" -> {
                    // Perform the action to enable auto-renew
                    enableAutoRenew(actionName)
                }
    
                // You can add more cases here for other actions
                else -> {
                    // Handle unknown actions or do nothing
                    print("Unknown action: $actionValue")
                }
            }
        }
    }
    ```
    
    #### Jetpack Compose
    
    ```
    import io.customer.messaginginapp.compose.InlineInAppMessage
    import io.customer.messaginginapp.type.InAppMessage
    import io.customer.messaginginapp.type.InlineMessageActionListener
    
    @Composable
    fun InlineInAppMessageExample() {
        InlineInAppMessage(
            elementId = <example-element-id>,
            modifier = Modifier
                .fillMaxWidth(),
            progressTint = Color(0xFF03DAC5), // Optional: set custom progress color if needed
            onAction = { message: InAppMessage, actionValue: String, actionName: String ->
                // Perform some logic when people tap an action button.
    
                // Example code handling button tap:
                when (actionValue) { // use actionValue or actionName, depending on how you composed the in-app message.
                    "enable-auto-renew" -> {
                        // Perform the action to enable auto-renew
                        enableAutoRenew(actionName)
                    }
    
                    // You can add more cases here for other actions
                    else -> {
                        // Handle unknown actions or do nothing
                        print("Unknown action: $actionValue")
                    }
                }
            }
        )
    }
    ```
    
2.  Register a global SDK event listener.
    
    When you register an [event listener](../in-app-event-listeners/#event-listeners) with the SDK, we’ll call the `messageActionTaken` event listener. We call this event listener for both modal and inline in-app message types, so you can reuse logic if you want.
    

## Handle responses to messages (event listeners)

Similar to [modal in-app messages](../in-app-event-listeners/#event-listeners), you can set up event listeners to handle your audience’s response to your messages.

For inline messages, you can listen for three different events:

*   `messageShown`: a message is “sent” and appears to a user.
*   `errorWithMessage`: the message itself produces an error—this probably prevents the message from appearing to the user.
*   `messageActionTaken`: the user performs an action in the message. As [shown above](#2-listen-for-events), this is only called if the View instance doesn’t have an `onActionDelegate` set.

Unlike modal in-app messages, you’ll notice that there’s no `messageDismissed` event. This is because inline messages don’t really have a concept of dismissal like modal messages do. They’re meant to be a part of your app!

---

## In app > Target in app messages

**Source:** /integrations/sdk/android/in-app/target-in-app-messages

# Page rules

Sending people in-app messages often depends on the screens they visit in your app.

You can set page rules when you create in-app messages. These rules determine the pages that your audience must visit in your app to see each message. Before you can take advantage of page rules, you need to:

1.  Track screens in your app. You can add `autoTrackActivityScreens(true)` to your `CustomerIO` configuration to automatically track screens or you can [track screens manually](/integrations/sdk/android/tracking/screen-events/#manual-screenview). **If you use Jetpack Compose** you should turn off automatic screen tracking and track screens manually.
2.  Provide screen names to whomever sets up in-app messages in the Customer.io UI. If we don’t recognize the page that you set for a page rule, your audience will never see your message.

The SDK automatically uses label in your manifest file as the page/screen name. If your screens don’t have labels, you won’t be able to set up page rules and screenview events will have an empty event `name`.

Make sure your screens use the same names across your apps

If you have a screen called `DashboardActivity` in Android, and `DashboardViewController` in iOS, we’ll recognize `Dashboard` as the screen for both platforms, making it easier for you to set page rules and track events for users across platforms.

 ![Set up page rules to limit in app messages by page](https://customer.io/images/in-app-page-rule.png)

Keep in mind: page rules are case sensitive. If you’re targeting your mobile app, make sure your page rules match the casing of the `name` in your `screen` events. If you’re targeting your website, your page rules should always be lowercase.

 ![The first page rule is Web contains /dashboard. The second page rule is iOS contains Dashboard.](https://customer.io/images/page-rule-case-sensitive.png)

---

## Live notifications > Reference

**Source:** /integrations/sdk/android/live-notifications/reference

# Live Updates API and payload reference

Your server drives every live notification through the [Live Notifications API](/integrations/api/app/tag/live-notifications/). This page covers the operations, the two objects you send with each one, the bundled templates, and the FCM data message that arrives on the device.

For the app-side work—enabling notification types, branding the templates, and rendering custom types—see [Set up Live Updates](/integrations/sdk/android/live-notifications/set-up/).

## Notification types

Every live notification belongs to a **notification type**. Each type has an `identifier` in reverse-DNS notation, like `io.customer.order-tracking`, so the identifier stays unique. Identifiers work well in code but are hard to scan in a list, so you can also assign a friendly `name` that appears in your activity logs.

A type is the *kind* of thing you track—an order, a ride, a match. Each running notification is an **instance** of that type, with its own `instance_id`. Your update, end, and status calls all reference a notification by its `instance_id`.

Customer.io records a notification type automatically when it receives the type’s SDK registration or its first start request. You can view your types and edit their friendly names under [**Settings** > **Live notifications**](https://fly.customer.io/workspaces/last/settings/live-notification-types). There’s no create or delete action, and you can’t edit an identifier.

 ![The live notification types table in workspace settings, showing each type's friendly name and reverse-DNS identifier](https://customer.io/images/live-activity-table.png)

## Activity lifecycle

Every live notification has a unique `instance_id` that you use to track its lifecycle and target its updates. Each instance holds every delivery made over the notification’s lifespan.

*   [When you start a notification from the API](/integrations/api/app/tag/live-notifications/startLiveNotification/), Customer.io generates the instance ID and returns it in the response.
*   When the notification starts from your app, the SDK generates it.

An instance can move through these states:

State

Meaning

`active`

The notification is running and can receive updates.

`ended`

Your app or server ended it.

`expired`

It passed its expiration without being ended.

`failed`

Customer.io couldn’t deliver to it.

### Start a notification

You can start a live notification two ways:

*   **From your app**: Your app starts the notification on the device—the moment a customer places an order, for example. This works while your app is in the foreground.
*   **From your server**: Your server calls the [start endpoint](/integrations/api/app/tag/live-notifications/startLiveNotification/) and Customer.io pushes to the device. Your app doesn’t need to be open.

Either way, the SDK reports the notification to Customer.io, and Customer.io pushes updates to it from that point on.

### Update a running notification

Your server updates live notifications through the [update endpoint](/integrations/api/app/tag/live-notifications/updateLiveNotification/), referencing the notification by its instance ID. Customer.io looks up the notification’s token and pushes the new content state to the device. The operating system re-renders it without launching your app.

Each update must carry the **full** content state. You can’t send partial updates that contain only the fields that changed.

Your app can also update a notification locally through the SDK. But SDK-side updates only change the notification on the device. The update doesn’t appear in the Customer.io delivery timeline. (Note that SDK-managed local starts and ends *do* appear in the delivery timeline.)

### End a notification

There are three ways to end a live notification:

*   **Your server** sends an [end request](/integrations/api/app/tag/live-notifications/endLiveNotification/) to Customer.io, which delivers a final push that transitions the notification to its ended state. You can include a final `content_state` to show before it ends; it defaults to the last state you sent.
*   **Your app** ends it locally, and the SDK reports the end to Customer.io.
*   **Automatic expiration.** Customer.io marks a notification `expired` 6 hours after it starts by default. You can set an earlier expiration with the `expiration` field on the [start request](/integrations/api/app/tag/live-notifications/startLiveNotification/), but 6 hours is the maximum. Expiration changes the status in Customer.io—it doesn’t send an end push or clear the notification from the device.

Server control and app control aren’t exclusive: you can start an activity in your app for instant feedback and hand it to your server for updates, or start it from your server and end it in your app. However, local updates never enter the delivery timeline, so a notification you drive from both places will have gaps in its Customer.io history.

### Review deliveries

Live notifications are a delivery channel in Customer.io. Every start, update, and end is a separate **delivery**, so one notification produces a timeline of deliveries over its lifespan. Each delivery moves through various statuses: `queued`, `sent`, `attempted`, `failed`, or `undeliverable`. In general, queued and sent deliveries are good states; failed and undeliverable deliveries mean your live notification didn’t make it to the device.

You can see every live notification sent to a person, and drill into each instance’s full delivery timeline, on their profile. See [Monitor live notifications](/messaging/channels/live-notifications/monitor/) for more.

## Attributes and content state

When you [start](/integrations/api/app/tag/live-notifications/startLiveNotification/), [update](/integrations/api/app/tag/live-notifications/updateLiveNotification/), or [end](/integrations/api/app/tag/live-notifications/endLiveNotification/) a live notification through the API, you send two objects: `attributes` and `content_state`. Customer.io wraps them in the right platform payload and delivers them to the device.

*   `attributes` describe the thing the notification is *about*—the order, the flight, the match. You set them when the notification starts, and they stay fixed for its lifetime.
*   `content_state` is what the notification *currently shows*—the status, the score, the ETA. It changes with every update, and the device re-renders from it each time.

When you design a notification type, that’s the test for where a field goes: if it can change while the notification is running, it belongs in `content_state`. If it identifies what the notification is tracking, it’s an attribute.

A few conventions apply:

*   **Dates and timestamps are epoch seconds**, not milliseconds. Use the Unix format.
*   **Each update carries the full content state.** The device re-renders from the complete state on every push. There are no partial updates.
*   **Static fields behave differently per platform.** On iOS, ActivityKit fixes `attributes` when the activity is created and keeps them for its lifetime, so you send them only on start. On Android, the device re-renders from each push, so re-send `attributes` on every update and end—otherwise the static fields disappear.

Send the static fields your renderer needs on every call

The Android SDK renders from a merged payload, so include any static fields your template or custom renderer reads in `attributes` on update and end calls too—not just on start.

## Built-in templates

The built-in templates are the lowest-effort way to ship a live notification: when one of the two shapes—a countdown or a step-by-step tracker—fits what you need, Customer.io renders it for you. You don’t have to write any UI code. You control the content the template shows (and, on Android, template branding like an accent color and logo), but the layout is fixed—you can’t rearrange a built-in template’s views. For anything else, you need to define a custom type and render it yourself.

Both built-in templates use the same [notification type identifiers](#notification-types) on iOS and Android, so one cross-platform notification can share a single `notification_type`. You send static fields in `attributes` and dynamic fields in `content_state`; each template below shows an example, followed by schemas for both objects. The fields the templates render verbatim (`header`, `title`, `status`, `substatus`) are freeform text, but the supported fields are only the ones in each schema—the bundled templates don’t expose generic status-color, image, or logo fields.

### Countdown timer

Use this template to create a countdown timer, with the notification type `io.customer.livenotifications.countdowntimer`. A complete start request wraps the template’s `attributes` and `content_state` (documented below) in the standard [start request](/integrations/api/app/tag/live-notifications/startLiveNotification/) fields—`identifiers` and `notification_type`:

```
{
    "identifiers": { "email": "person@example.com" },
    "notification_type": "io.customer.livenotifications.countdowntimer",
    "attributes": {
        "header": "Summer Sale"
    },
    "content_state": {
        "title": "Flash sale ends in",
        "statusMessage": "30% off everything",
        "endTime": 1753603600
    }
}
```

#### Attributes

*   headerstringrequired
    
    Top-row label of the notification.
    

#### Content state

*   titlestringrequired
    
    Primary status line.
    
*   statusMessagestring
    
    Secondary line under the title.
    
*   endTimeinteger
    
    Countdown target, in whole seconds since 1970 UTC. A time in the future renders a live countdown; omit it to render no timer. The countdown does not clear itself when it reaches zero — it rests at \\"0:00\\" until you send an update with a finished title and no endTime.
    

### Multi-step tracker

Use this template to create a delivery process tracker, or any tracker with steps, with the notification type `io.customer.livenotifications.segments`. A complete start request wraps the template’s `attributes` and `content_state` (documented below) in the standard [start request](/integrations/api/app/tag/live-notifications/startLiveNotification/) fields—`identifiers` and `notification_type`:

```
{
    "identifiers": { "email": "person@example.com" },
    "notification_type": "io.customer.livenotifications.segments",
    "attributes": {
        "header": "Chica's Tacos"
    },
    "content_state": {
        "status": "Preparing your order",
        "substatus": "We'll let you know when it's on the way",
        "segmentsTotal": 4,
        "segmentsComplete": 2,
        "trailingText": "25 min"
    }
}
```

Android shows one body line

Android has a single body slot, so `trailingText` appears only when `substatus` is empty. When you set both, `substatus` wins and `trailingText` is dropped.

#### Attributes

*   headerstringrequired
    
    Top-row label
    

#### Content state

*   statusstringrequired
    
    Primary status line, like `Out for delivery`.
    
*   substatusstring
    
    Secondary line under the status.
    
*   segmentsTotalintegerrequired
    
    The total number of segments in the progress bar. Values above 20 are capped at 20.
    
*   segmentsCompleteintegerrequired
    
    How many segments are filled; the remainder render as incomplete. Values above segmentsTotal are capped at segmentsTotal.
    
*   trailingTextstring
    
    Short text on the Dynamic Island trailing edge, e.g. \\"5 min\\". Keep it brief; the trailing region is narrow.
    

## What Customer.io sends to FCM

Android live notifications arrive as FCM **data messages**. There’s no `notification` block, so the SDK renders them even when your app is in the background:

```
{
    "data": {
        "cioInstanceId": "01J4YQZC3GJ0S6RY4E5NW6H9AB",
        "event": "update",
        "notification_type": "io.customer.livenotifications.segments",
        "timestamp": "1721000000",
        "link": "yourapp://orders/1234",
        "payload": "{\"header\":\"Order\",\"status\":\"Out for delivery\",\"segmentsTotal\":4,\"segmentsComplete\":3}"
    }
}
```

*   `cioInstanceId` identifies the notification. Updates with the same ID replace the notification in place.
*   `event` is `start`, `update`, or `end`.
*   `notification_type` selects the template, or your custom renderer.
*   `timestamp` is what the SDK uses to discard out-of-order and duplicate deliveries.
*   `payload` is a JSON string of your merged `attributes` and `content_state`.

---

## Live notifications > Set up

**Source:** /integrations/sdk/android/live-notifications/set-up

# Set up Live Updates

Live Updates push real-time, continuously updated notifications to Android devices—a delivery tracker, a live score, a countdown. Use the SDK's built-in templates or render your own, and let Customer.io push updates to the notification shade.

Minimum requirements

Live Updates require the `messagingpush` module. You need [push set up with FCM](/integrations/sdk/android/push/), notification permission (`POST_NOTIFICATIONS` on Android 13+), and an identified profile. One optional permission unlocks Android 16’s promoted styling—see the [permissions you’ll need](#before-you-begin) below.

## How it works

A live notification is an ongoing notification that updates in place—one notification whose content changes as the order moves, the score changes, or the timer counts down. On Android 16 and later, it renders with the platform’s promoted [Live Updates ↗](https://developer.android.com/develop/ui/views/notifications/live-update) treatment. On earlier versions it falls back to a standard ongoing notification carrying the same content, so your customers still see every update—just without the promoted styling.

The Android SDK ships **built-in templates** and renders them for you. It bundles two: a multi-step tracker (`Segments`) for delivery or any step-based progress, and a countdown timer. For anything else, define a custom type and render the notification in your own code.

Four actors drive every live notification:

*   **You** enable the notification types your app supports, and supply a renderer for any custom types.
*   **The SDK** renders each update—built-in templates directly, your custom types through your renderer—and reports the starts and ends your app drives, plus a customer’s dismissal, to Customer.io. Local updates change the notification on the device only.
*   **Your server** starts, updates, and ends notifications by calling [the API](/integrations/api/app/tag/live-notifications/).
*   **Customer.io** delivers each update to the device and records every start, update, and end as a delivery.

Customer.io sits in the middle: your server only ever calls the API, and Customer.io delivers each update to the device, where the SDK renders it—with a built-in template or your own renderer. For the API operations, payload shapes, and template fields, see the [API and payload reference](/integrations/sdk/android/live-notifications/reference/).

## Before you begin

Live Updates use the FCM credentials your app already has in Customer.io. Two permissions matter:

Permission

Required

What it does

`POST_NOTIFICATIONS`

Yes, on Android 13 and later

Lets your app display notifications at all

`POST_PROMOTED_NOTIFICATIONS`

No

Gets Android 16’s promoted **Live Updates** treatment. Without it, live notifications still render as ongoing notifications on Android 16.

Declare the promoted permission in your app’s manifest:

```
<uses-permission android:name="android.permission.POST_PROMOTED_NOTIFICATIONS" />
```

You must also identify your app user before you can start a notification. The SDK holds token registrations until you identify a person, and [the API](/integrations/api/app/tag/live-notifications/startLiveNotification/) rejects requests that target anonymous profiles.

## Step 1: Enable notification types

You must enable at least one type of live notification in your push module configuration. Enable built-in template types with `enableLiveNotificationTypes`, and your own app-rendered types with `enableCustomLiveNotificationTypes`. The calls are additive—use either or both.

Add the module when you initialize the SDK, and keep a reference if you plan to start live notifications locally.

```
import io.customer.messagingpush.MessagingPushModuleConfig
import io.customer.messagingpush.ModuleMessagingPushFCM
import io.customer.messagingpush.livenotification.LiveNotificationType

val pushModule = ModuleMessagingPushFCM(
    MessagingPushModuleConfig.Builder()
        // Built-in templates, rendered by the SDK
        .enableLiveNotificationTypes(
            LiveNotificationType.SEGMENTS,
            LiveNotificationType.COUNTDOWN_TIMER
        )
        // Custom types, rendered by your callback (step 3)
        .enableCustomLiveNotificationTypes("io.yourapp.workout")
        .build()
)
```

Each built-in type has a reverse-DNS identifier that your server uses to target it:

Type

Identifier

`SEGMENTS`

`io.customer.livenotifications.segments`

`COUNTDOWN_TIMER`

`io.customer.livenotifications.countdowntimer`

The identifiers match the iOS sample types, so a cross-platform notification can share one `notification_type` server-side. Each template’s fields are documented in the [API and payload reference](/integrations/sdk/android/live-notifications/reference/#built-in-templates).

## Step 2: Add branding (optional)

Built-in templates accept app-level branding—an accent color, a small icon, and a logo applied to every live notification the SDK renders from a template.

```
import io.customer.messagingpush.livenotification.LiveNotificationAsset
import io.customer.messagingpush.livenotification.LiveNotificationBranding

MessagingPushModuleConfig.Builder()
    .setLiveNotificationBranding(
        LiveNotificationBranding(
            companyName = "Your Company",
            accentColor = Color.parseColor("#1B5E20"),
            smallIcon = R.drawable.ic_notification,
            logo = LiveNotificationAsset.Drawable(R.drawable.brand_logo)
        )
    )
```

*   `accentColor` sets the notification’s accent color.
*   `smallIcon` sets the **small icon** (the status-bar glyph) for live notifications. It must be a bundled drawable resource.
*   `logo` sets the logo the template renders as the large icon. Wrap a bundled drawable in `LiveNotificationAsset.Drawable`.

## Step 3: Render custom types

If you want to send a notification without a built-in template, you must render it yourself. Implement `createLiveNotification` and return a complete `Notification`—or `null` to fall back to the SDK’s template for built-in types.

```
import io.customer.messagingpush.data.model.CustomerIOParsedPushPayload
import io.customer.messagingpush.livenotification.CustomerIOLiveNotificationsCallback

class MyLiveNotificationCallback : CustomerIOLiveNotificationsCallback {
    override fun createLiveNotification(
        payload: CustomerIOParsedPushPayload,
        context: Context
    ): Notification? {
        val extras = payload.extras
        // Return null for types you don't render—the SDK's templates
        // handle the built-in ones.
        if (extras.getString("notification_type") != "io.yourapp.workout") return null
        val ended = extras.getString("event") == "end"
        return NotificationCompat.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_workout)
            .setContentTitle(extras.getString("title"))
            .setOngoing(!ended)
            .build()
    }
}
```

The notification’s flattened fields arrive in `payload.extras` with `notification_type` and the `event` (`start`, `update`, or `end`). The SDK posts your notification using the activity ID, so updates replace it in place. On end, it renders a final non-ongoing notification when content is available; otherwise it cancels the existing notification. Register the callback with `setLiveNotificationCallback` on the module config builder.

Because the SDK renders from a merged payload, include any static fields your renderer needs in `attributes` on update and end calls too—not just on start.

That’s the setup. To create your first live notification—from your server or from your app—see [Start Live Updates](/integrations/sdk/android/live-notifications/start/).

---

## Live notifications > Start

**Source:** /integrations/sdk/android/live-notifications/start

# Start Live Updates

Once you’ve [set up Live Updates](/integrations/sdk/android/live-notifications/set-up/), there are two ways to start a live notification:

*   **From your server:** You call [the API](/integrations/api/app/tag/live-notifications/startLiveNotification/) when you’re ready to start a live notification, and Customer.io delivers an FCM data message. The SDK renders the notification without your app needing to be open. This is the most common path for notifications like delivery tracking and live scores.
*   **From your app:** Start the notification locally through the push module. Built-in types use typed data classes; custom types take a field map. This is a common path for notifications like a workout progress tracker.

## From your server

Call the API to [start](/integrations/api/app/tag/live-notifications/startLiveNotification/) and [update](/integrations/api/app/tag/live-notifications/updateLiveNotification/) a live notification. Customer.io delivers the FCM data message, and the SDK renders the notification from your payload’s fields. See the [API and payload reference](/integrations/sdk/android/live-notifications/reference/) for each template’s fields.

## From your app

Start the notification locally through the push module:

```
import io.customer.messagingpush.livenotification.LiveNotificationData

// Built-in template type
val activityId = pushModule.startLiveNotification(
    LiveNotificationData.Segments(
        header = "Order",
        status = "Order received",
        segmentsTotal = 4,
        segmentsComplete = 1
    )
)
```

`startLiveNotification` returns the notification’s generated ID and registers it with Customer.io, so your server can take over updates from there. Local starts and ends are reported to Customer.io automatically. Local updates change the notification on the device but don’t appear in the Customer.io delivery timeline.

Then you can update the notification and end it later with the returned ID:

```
// Update and end it later with the returned id
pushModule.updateLiveNotification(
    activityId,
    LiveNotificationData.Segments(
        header = "Order",
        status = "Out for delivery",
        segmentsTotal = 4,
        segmentsComplete = 3
    )
)
pushModule.endLiveNotification(activityId)
```

For the full lifecycle, states, instance IDs, and how deliveries appear in Customer.io, see the [API and payload reference](/integrations/sdk/android/live-notifications/reference/).

## How the SDK handles tokens and updates

Live Updates reuse the FCM registration token your app already uses for push. There are no separate token types to manage. The SDK registers tokens for **identified people** only—if a customer hasn’t been identified yet, it holds the registration and sends it when they are.

Live notifications arrive as FCM **data messages**, not display notifications, so the SDK renders them directly and updates the notification in place by its instance ID. Because Android renders each message as it arrives, the SDK guards against out-of-order and duplicate deliveries using the message timestamp.

Two things end a notification’s lifecycle on the device beyond your server’s end call:

*   **A person dismisses the notification.** When someone swipes the notification away, the SDK ends the activity and clears it.
*   **A person logs out.** The SDK ends and clears every live notification on the device.

---

## Push > Deep links

**Source:** /integrations/sdk/android/push/deep-links

# Deep links

Our SDK supports redirects for notification links [registered in Android ↗](https://developer.android.com/training/app-links/deep-linking) by default. You can customize this behavior using `CustomerIOPushNotificationCallback`.

*   To register a deep link, you must first add [intent filters ↗](https://developer.android.com/training/app-links/deep-linking#adding-filters) in your `AndroidManifest.xml` file.
    
    ```
    <intent-filter android:label="deep_linking_filter">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- Accepts URIs that begin with "remote-habits://settings” -->
        <data
            android:host="settings"
            android:scheme="remote-habits" />
    </intent-filter>
    ```
    
*   `CustomerIOPushNotificationCallback`—a URL handler feature provided by the SDK. When configuring your `CustomerIO` instance, you can set the callback to handle notification behavior.
    
    Note that the if statement (`payload.deeplink.doesNotMatch()`) in the example below is just an example of what you might do if you handle some links with our SDK and handle others yourself.
    
    ```
    class MainApplication1 : Application(), CustomerIOPushNotificationCallback {
        override fun onCreate() {
            super.onCreate()
            val builder = CustomerIOConfigBuilder(
                applicationContext = this,
                cdpApiKey = "your-cdp-api-key"
            ).autoTrackActivityScreens(true)
                .addCustomerIOModule(
                    ModuleMessagingPushFCM(
                        moduleConfig = MessagingPushModuleConfig.Builder().apply {
                            setNotificationCallback(this)
                        }.build()
                    )
                )
    
            CustomerIO.initialize(builder.build())
        }
    
        override fun onNotificationClicked(payload: CustomerIOParsedPushPayload, context: Context): Unit? {
            // This if statement is an example of what you might do to handle 
            // some links with our SDK and others yourself.
            if (payload.deepLink.doesNotMatch()) {
                // Return null so CustomerIO SDK can handle Notification Clicked
                return null
            }
            // Custom handling of Notification Clicked
            return Unit
        }
    }
    ```
    

Tip

When someone taps a push notification with a deep link, the SDK calls the `CustomerIOPushNotificationCallback` specified in `CustomerIOBuilder` object before it looks for default supported links.

The `onNotificationClicked` function lets you override the SDK’s default click handler. If you return `null`, the SDK will handle the click normally, opening the link in your app or web browser. Otherwise, you can handle the click yourself using the `payload` and `context` parameters.

Don't forget to capture metrics

When you provide `CustomerIOPushNotificationCallback`, don’t forget to [capture notification metrics](/integrations/sdk/android/push/#capture-push-metrics), otherwise our dashboards won’t record conversions properly!

---

## Push > Push

**Source:** /integrations/sdk/android/push/push

# Push notifications

Get started setting up push notifications for Android. Our Android SDK supports push notifications over FCM, including rich push messages with links and images.

## Before you begin

This page explains how to receive rich push notifications using our SDK. However, before you can send push notifications to your audience, you need to enable Customer.io to send push notifications through [Firebase Cloud Messaging (FCM)](/messaging/channels/push/getting-started/#for-android).

### How it works

Before a device can receive a push notification, you must:

1.  Set up [FCM](/messaging/channels/push/getting-started/#for-android).
2.  [Set up push](#set-up-push).
3.  [Identify a profile](/integrations/sdk/android/identify/). When someone starts the app, they automatically generate a device token. Identifying the profile associates the device token with the profile in Customer.io, so that they can receive push notifications.
4.  Set up an automation to send a push notification through the Customer.io composer.

## Set up push

1.  You must implement the Push Messaging SDK to use push notification features.
    
    ```
    implementation 'io.customer.android:messaging-push-fcm:4.20.3'
    ```
    
2.  Initialize the push module. The push module has an [optional config object](#push-module-config), explained below. See [deep links](#deep-links) for help configuring links.
    
    ```
     val builder = CustomerIOConfigBuilder(
         applicationContext = this,
         cdpApiKey = "your-cdp-api-key"
     ).autoTrackActivityScreens(true)
         .addCustomerIOModule(
             ModuleMessagingPushFCM()
         )
         .region(Region.US)
    
     CustomerIO.initialize(builder.build())
    ```
    

The SDK adds a `FirebaseMessagingService` to the app manifest automatically, so you don’t have to perform additional setup to handle incoming push messages.

However, if your application implements its own `FirebaseMessagingService`, make sure that when you call `onMessageReceived` and `onNewToken` methods, you also call `CustomerIOFirebaseMessagingService.onMessageReceived` and `CustomerIOFirebaseMessagingService.onNewToken` respectively.

```
class FirebaseMessagingService : FirebaseMessagingService() {

 override fun onMessageReceived(message: RemoteMessage) {
    val handled = CustomerIOFirebaseMessagingService.onMessageReceived(context, message)
    if (handled) {
        logger.breadcrumb(this, "Push notification has been handled", null)
    }
 }

override fun onNewToken(token: String) {
    CustomerIOFirebaseMessagingService.onNewToken(context, token)
}
```

Push notifications launched from the SDK are currently posted to our default channel—`[your app name] Channel`. In the future, we plan to let you customize channels/categories so that users can subscribe and unsubscribe to content categories as necessary.

### Push module configuration

`ModuleMesagingPushFCM` has an optional configuration object. In most cases, our default configuration works, but you can pass the configuration object to customize the way you handle push notifications and so on.

Config option

Default

Description

`notificationCallback`

`null`

A callback that notifies the client on push notification related actions. This lets you override the default behavior for push notifications.

`autoTrackPushEvents`

`true`

Boolean: when true, the SDK automatically tracks push events like `delivered` and `opened`.

`pushClickBehavior`

`ACTIVITY_RESTART`

Lets you customize the behavior when a user taps a push notification. See [push click behavior](#push-click-behavior).

```
val builder = CustomerIOConfigBuilder(
    applicationContext = this,
    cdpApiKey = "your-cdp-api-key",
).autoTrackActivityScreens(true)
    .addCustomerIOModule(
        ModuleMessagingPushFCM(
            moduleConfig = MessagingPushModuleConfig.Builder().apply {
                setNotificationCallback(this)
            }.build()
        )
    )
    .region(Region.US)

CustomerIO.initialize(builder.build())
```

### Push click behavior

The `pushClickBehavior` config lets you customize your application’s response when your audience taps a push notification. This includes going to specific deep links or launcher screens based on the notification payload. Note that the SDK tracks `opened` metrics for all click behaviors.

```
builder.addCustomerIOModule(
  ModuleMessagingPushFCM(
    moduleConfig = MessagingPushModuleConfig.Builder().apply {
      setPushClickBehavior(PushClickBehavior.ACTIVITY_PREVENT_RESTART)
    }.build()
  )
)
```

The available options are:

*   `ACTIVITY_PREVENT_RESTART` **(Default)**: If your app is already in the foreground, the SDK will not re-create your app when your audience clicks a push notification. Instead, the SDK will reuse the existing activity. If your app *is not* in the foreground, we’ll launch a new instance of your deep-linked activity. We recommend that you use this setting if your app has screens that your audience shouldn’t navigate away from—like a shopping cart screen.
    
*   `ACTIVITY_NO_FLAGS`: If your app is in the foreground, the SDK will re-create your app when your audience clicks a notification. The activity is added on top of the app’s existing navigation stack, so if your audience tries to go back, they will go back to where they previously were.
    
*   `RESET_TASK_STACK`: No matter what state your app is in (foreground, background, killed), the SDK will re-create your app when your audience clicks a push notification. Whether your app is in the foreground or background, the state of your app will be killed so your audience cannot go back to the previous screen if they press the back button.
    

## Capture push metrics

Customer.io supports device-side metrics that help you determine the efficacy of your push notifications: `delivered` when a push notification is received by the app and `opened` when a push notification is clicked.

By default, the `messaging-push-fcm` package automatically tracks `opened` and `delivered` for push notifications originating from Customer.io. Otherwise, you can track push metrics with the `trackMetric` method.

```
CustomerIO.instance().trackMetric(
    deliveryID = deliveryId,
    deviceToken = deliveryToken,
    event = MetricEvent.delivered
)
```

## Customizing Push Notifications

You can customize the icon and color of push notifications by updating your [`AndroidManifest` as recommended by FCM ↗](https://firebase.google.com/docs/cloud-messaging/android/client#manifest).

```
<meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_notification" />
<meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/colorNotificationIcon" />
```

However, if you want more control over your notifications’ appearance and behavior, Customer.io SDK provides an option to override these settings on the app side. You can customize notification appearance by implementing `CustomerIOPushNotificationCallback` and overriding the `onNotificationComposed` method.

```
class MainApplication : Application(), CustomerIOPushNotificationCallback {
    override fun onCreate() {
        super.onCreate()
        val builder = CustomerIOConfigBuilder(
            applicationContext = this,
            cdpApiKey = "your-cdp-api-key",
        ).autoTrackActivityScreens(true)
            .addCustomerIOModule(
                ModuleMessagingPushFCM(
                    moduleConfig = MessagingPushModuleConfig.Builder().apply {
                        setNotificationCallback(this)
                    }.build()
                )
            )
        CustomerIO.initialize(builder.build())
    }

    override fun onNotificationComposed(
        payload: CustomerIOParsedPushPayload,
        builder: NotificationCompat.Builder
    ) {
        // Customize your notification here
    }
}
```

You cannot override `PendingIntent` for notifications. If you want to override the behavior when people tap your notifications, you can implement `onNotificationClicked` as [described in our deep links documentation](/integrations/sdk/android/push/deep-links/).

### The push notification icon

You’ll set the icon that appears on normal push notifications as a part of your app manifest. If your icon appears in the wrong size, or if you want to change the standard icon that appears with your push notifications, you’ll need to update your app’s manifest.

```
<meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_notification" />
<meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/colorNotificationIcon" />
```

---

## Push > Push certificates

**Source:** /integrations/sdk/android/push/push-certificates

# Push service certificates

## Upload your push certificate

If you don’t already have your [FCM `.JSON` file](#get-your-json-file-for-fcm), you’ll need to get it before you can finish this process and send push notifications.

1.  In Customer.io, go to [\> *Workspace Settings* > *Push*.](https://fly.customer.io/workspaces/last/settings/actions/apps).
    
2.  Click **Add app** and give your app a name—or click an existing app to add a platform to it.
    
3.  Select the *iOS* platform, and choose *Firebase Cloud Messaging (FCM)*. An app’s iOS and Android platforms can share a single FCM credential.
    

 ![Push Settings - iOS_provider (FCM)](https://customer.io/images/push_settings_ios_provider.png)

## Get your .JSON file for FCM

Before you can get a push certificate for Firebase Cloud Messaging, make sure that the FCM API is enabled for your project. You can [check that here ↗](https://console.developers.google.com/apis/api/fcm.googleapis.com/overview).

1.  Log into the Firebase Console for your project.
    
2.  Click in the sidebar and go to **Project settings**.
    
    ![Access your project settings in firebase](https://customer.io/images/project-settings.png)
    
3.  Go to **Service Accounts** and click **Generate New Private Key**. Confirm your choice and download the credential file.
    
    ![Generate a new private key in Firebase](https://customer.io/images/generate-key.png)

---

## Push > Push notification channel

**Source:** /integrations/sdk/android/push/push-notification-channel

# Push notification channel

🎉New in v4.7.0

Starting in Android 8.0, you can set up “notification channels,” which categorize notifications for your Android app. Every notification now belongs to a channel and the channel determines the behavior of notifications—whether they play sounds, appear as heads-up notifications, and so on. Channels also give users control over which channels they want to see notifications from. For example, if you had a news app, you might have different channels for sports, entertainment, and breaking news, giving users the ability to pick the channels they care about.

Today, Customer.io supports **a single channel per app**, and it has three settings, listed in the table below. You can customize your channel when you first set up the Customer.io SDK, but you cannot change the channel ID or importance level after you’ve created a channel. You can only change the channel name. [Learn more from the official Android developer docs ↗](https://developer.android.com/develop/ui/views/notifications/channels).

Channels are created on the audience’s side when they receive their first push from Customer.io. Users can see your channel in their device settings.

Channel setting

Default

Description

Channel ID

`[your package name]`

The ID of the channel.

Channel name

`[your app name] Notifications`

The name of the channel.

Importance

`3`

The importance of the channel. Acceptable values are `0` (min), `1` (low), `2` (medium), `3` (default/high), and `4` (urgent). See [the Android developer documentation ↗](https://developer.android.com/develop/ui/views/notifications/channels#importance) for more about the behavior of each importance level.

## Channel configuration

When you first integrate with the Customer.io SDK, you can set up your Android channel. Remember, after you’ve released a version of your app with channel settings, you can only change the channel name. Changes to other settings have no effect.

You’ll customize your channel in your app’s manifest.

```
<manifest>
    <application>
        <meta-data
            android:name="io.customer.notification_channel_id"
            android:value="channel_id_value" />
        <meta-data
            android:name="io.customer.notification_channel_name"
            android:value="Channel Name" />
        <meta-data
            android:name="io.customer.notification_channel_importance"
            android:value="4" />
    </application>
</manifest>
```

### What channel settings can I change?

When you first set up the Customer.io Android SDK, you can customize your channel. But after you release a version of your app with the Customer.io SDK, you cannot change the channel ID or importance level. After that, you can only change the channel name. This is a limitation imposed by Android, not Customer.io.

If you released your app with a version of the Customer.io Android SDK prior to 4.7.0, you can delete your old channel and create a new one with completely new settings per [Android's developer documentation](https://developer.android.com/develop/ui/views/notifications/channels#DeleteChannel).

The chart below shows what channel settings you can or can't change:

## Delete a channel

If you’ve released a version of your app with the Customer.io SDK earlier than v4.7.0, you can delete your old channel and create a new one with completely new settings per [Android’s developer documentation ↗](https://developer.android.com/develop/ui/views/notifications/channels#DeleteChannel).

```
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val id: String = context.packageName
notificationManager.deleteNotificationChannel(id)
```

---

## Push > Test push

**Source:** /integrations/sdk/android/push/test-push

# Test your push implementation

This is what the payload looks like on our end. If you’ve set up your app to use other data—custom keys outside the scope of our SDK—you can use our Custom Payload Editor; you’re welcome to place custom keys inside the `message.data` object, but you’ll need to do additional development to support keys beyond our standard `title`, `body`, `link`, and `image`.

```
{
  "message": {
    "data": {
      "title": "string", //(optional) The title of the notification.
      "body": "string", //The message you want to send.
      "image": "string", //https URL to an image you want to include in the notification
      "link": "string" //Deep link in the format remote-habits://deep?message=hello&message2=world
    }
  }
}
```

*   messageone ofrequired
    
    The parent object for all push payloads.

---

## Tracking > Anonymous activity

**Source:** /integrations/sdk/android/tracking/anonymous-activity

# Anonymous activity

Before you identify a profile, calls you make to the SDK are associated with an `anonymousId`. When you identify that profile, we reconcile their anonymous activity with the identified profile.

In Customer.io, you’ll see anonymous activity in the *Activity Log*, but we don’t store anonymous activities on a [profile](/messaging/profiles/manage/overview/) until the person is identified by an email address or other ID. You won’t be able to find an “anonymous profile” in your workspace.

You can’t trigger automations or send messages like push notifications to anonymous users; however, if you’re on a premium or enterprise plan, you can send [anonymous in-app messages](/integrations/sdk/android/in-app/in-app/#anonymous-messages).

When you identify a person, we merge anonymous activity with the identified person. And then the identified person’s previously anonymous activity *can* trigger automations and cause your audience to receive messages.

For example, imagine that you have an ecommerce app, and you want to message people who view a specific product. An anonymous user looks at the product in question, goes to a different page, and then logs into your app. When they log in, we merge their anonymous activity including their `screen` view. This triggers the automation you set up for people who visited the product page.

You can return a profile’s anonymous ID at any time by calling `CustomerIO.instance().anonymousId`.

---

## Tracking > Geofencing

**Source:** /integrations/sdk/android/tracking/geofencing

# Geofencing

## How it works

**Geofences** let you react when a person enters or exits a real-world location—a store, a venue, a neighborhood. You define geofences in Customer.io. The SDK downloads them, monitors them on the device, and automatically sends a `Geofence Transition` event to Customer.io when your users—represented by profiles in Customer.io—cross into (or out of) a geofence. You can use that event to trigger journeys and build segments, the same way you use any other event.

In Customer.io, you group geofences into **geofence sets**. A geofence set is a group of geofences that you act on together. For example, you might group all of your stores into a geofence set, and trigger an automation when a device enters any of the geofences in the set. Each `Geofence Transition` event includes both the geofence crossed and the geofence set it belongs to.

The SDK relies on the operating system’s native geofencing so transitions can arrive while your app is in the background (subject to permissions).

Geofencing depends on the [Location module](/integrations/sdk/android/tracking/location/), which supplies the device’s current location. This tells the SDK which geofences to monitor.

Event payloads exclude coordinates

For privacy, `Geofence Transition` events don’t include the profile’s precise latitude and longitude—only whether they entered or exited a geofence.

## Prerequisites

Geofencing requires:

*   The **Geofence module** (`io.customer.android:geofence`).
*   The **Location module**, which the geofence dependency includes automatically. See [location tracking](/integrations/sdk/android/tracking/location/).
*   Location permissions from the person using your app.
*   Location services enabled on the device.
*   Google Play Services on the device.

## 1\. Install the geofence module

Add the `geofence` dependency to your app’s `build.gradle` file:

```
implementation "io.customer.android:geofence:<version-here>"
```

The geofence dependency includes the location module automatically, so you don’t need to add `io.customer.android:location` separately. It’s fine to keep an explicit `location` dependency if you already added one for [location tracking](/integrations/sdk/android/tracking/location/).

## 2\. Add location permissions

Add the following permissions to your `AndroidManifest.xml` and request them from the person using your app:

```
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
```

The SDK doesn’t request permissions on its own—your app must handle the runtime permission flow.

### You want background location permissions

Geofencing requires your user to grant location permissions. In general, you want your users to set **Allow all the time** for the background location permission, so that your app can detect geofence transitions when your app is closed or in the background.

While `ACCESS_FINE_LOCATION` alone is enough to register geofences, and that may give the appearance that geofencing is working, Android only delivers transitions while your app is in the foreground. Since people rarely have your app open at the moment they arrive somewhere, foreground-only access will miss most geofence transitions.

Permission

Behavior if missing

`ACCESS_FINE_LOCATION`

Geofencing is effectively disabled. The SDK can’t register geofences, and logs `Cannot register geofences: ACCESS_FINE_LOCATION not granted`. Coarse location isn’t a substitute.

`ACCESS_BACKGROUND_LOCATION`

Transitions arrive only while your app is in the foreground. The SDK logs `ACCESS_BACKGROUND_LOCATION not granted — transitions will only fire while the app is in the foreground`.

`RECEIVE_BOOT_COMPLETED`

Geofences aren’t restored after a reboot until the person opens your app again.

Background location is a separate grant on Android 10 (API level 29) and later. On earlier versions, `ACCESS_FINE_LOCATION` covers background delivery on its own.

### Requesting permissions

On Android 11 (API level 30) and later, you can’t request access to background location in the same prompt as foreground location. That means you need to ask for background location in two steps.

1.  Request `ACCESS_FINE_LOCATION` when someone reaches a part of your app where location matters, and explain what they get in return.
2.  Request `ACCESS_BACKGROUND_LOCATION` later, at a moment when the benefit is obvious. On Android 11 and later, this takes the person to system settings, where they choose **Allow all the time** themselves—so tell them what to pick before you send them there.

Call `refreshFromCurrentLocation()` when the user grants the background location permission, so the SDK picks up nearby geofences without waiting for its next scheduled refresh.

If the user downgrades to foreground-only access later, the SDK keeps its registrations but the operating system won’t wake your app when transitioning in or out of a geofence.

Google Play also requires a [declaration and review ↗](https://support.google.com/googleplay/android-developer/answer/9799150) for apps that request background location. You should plan to spend time on this before you release your app.

## 3\. Initialize the SDK with the geofence module

Register both `ModuleLocation` and `ModuleGeofence` when you initialize the SDK. You don’t need to turn on [location tracking](/integrations/sdk/android/tracking/location/) for geofencing—by default, the geofence module acquires the location it needs on its own. See [location modes](#location-modes-and-monitoring) below.

```
val builder = CustomerIOConfigBuilder(applicationContext, "your-cdp-api-key")
    // ...
    .addCustomerIOModule(ModuleLocation())
    .addCustomerIOModule(ModuleGeofence())
    .build()

CustomerIO.initialize(builder)
```

## The Geofence Transition event

The SDK sends the `Geofence Transition` event automatically when a person enters or exits a geofence. You don’t need to track enter or exit transitions yourself.

When a person crosses a geofence, the SDK sends one event for each geofence set the geofence belongs to, each carrying that set’s `geosetId`. A geofence that isn’t in any geofence set produces a single event without a `geosetId`.

The event’s timestamp reflects when the device received the transition event and preserves that timestamp if delivery is delayed. So a transition captured while the device is offline still lands on the timeline at the moment it occurred.

Property

Description

`transition`

The direction the device crossed the geofence boundary—`enter` or `exit`.

`geofenceId`

Customer.io-generated ID of the geofence. This is different from the `external_id` used for geofence imports.

`geofenceName`

The geofence’s name. Included only when the geofence has one.

`geosetId`

Customer.io-generated ID of the geofence set.

`metadata`

The metadata you define on the geofence in Customer.io. Values keep their types—a numeric value arrives as a number, not a string. Metadata values must be JSON scalars (`string`, `number`, or `boolean`); the SDKs drop array and object values. If the geofence has no metadata, the property is an empty object.

`transitionId`

A unique ID for the transition into or out of the geofence. Events sent for different geofence sets share the same ID, and it stays stable if the SDK retries delivery. To de-duplicate transition events, use `transitionId` and `geosetId` together.

Example event:

```
{
  "name": "Geofence Transition",
  "timestamp": "2026-07-06T17:42:10.482Z",
  "properties": {
    "transition": "enter",
    "geofenceId": "48122",
    "geofenceName": "Downtown flagship store",
    "geosetId": "25678",
    "transitionId": "b3f1c2a4-8e7d-4c2b-9f10-2a6f1e0d5c33",
    "metadata": {
      "region": "northeast",
      "priority": 2
    }
  }
}
```

## Location modes and monitoring

Devices can only monitor a limited number of geofences at once, so the SDK monitors the geofences nearest to the user, ranked by distance from the device’s location. The SDK automatically refreshes that set as the device moves. Geofence sets don’t affect monitoring—the SDK ranks individual geofences, and geofence set membership only determines the events it sends.

The geofence module needs a location fix to select that set. The `locationMode` in the geofence module’s config controls how it gets one:

Location mode

Behavior

`AUTOMATIC` (default)

The SDK acquires a location fix on its own whenever geofencing needs one and none is already available—for example, when you identify a profile.

`MANUAL`

The SDK doesn’t acquire location on its own. Call `ModuleGeofence.instance().refreshFromCurrentLocation()` after you identify someone and they grant location permission.

```
ModuleGeofence(
    GeofenceModuleConfig.Builder()
        .setLocationMode(GeofenceLocationMode.AUTOMATIC)
        .build()
)
```

Location the SDK acquires for geofencing is used for geofencing only—it never produces a `Location Update` event or updates the profile. It also works independently of the Location module’s tracking mode, even when location tracking is off.

`refreshFromCurrentLocation()` works in either mode—call it any time you want to force an immediate refresh. Geofences are tied to the identified profile, so in `MANUAL` mode call it after you identify someone; a refresh requested before you identify anyone isn’t retried automatically.

Neither mode limits detection to the moment the SDK acquires a location. Once the SDK registers geofences, the operating system monitors them natively—it wakes your app when the device crosses one, in the background and even between launches, without continuously tracking their location.

The device won't see new geofences immediately.

Any changes you make to geofences or geofence sets in Customer.io won’t be available on the device until it fetches updates from Customer.io. This happens within 24 hours for an active app, or sooner if the person travels a long distance, or your app forces a refresh. See [Geofence refresh](#geofence-refresh).

### Geofence refresh

The SDK re-downloads your geofence definitions from Customer.io—picking up any geofences you’ve added, changed, or removed, along with the geofence sets they belong to, when:

*   **Your app launches, or you identify a person**—at most once every 24 hours. Geofencing needs an identified profile, so if no one is identified when your app launches, the fetch waits until you identify someone.
*   **The person travels a long distance** (about 5 km by default) from where the SDK last fetched.
*   **Your app forces a refresh** by calling `refreshFromCurrentLocation()`.

So changes you make in Customer.io aren’t instant. They reach a device the next time one of these happens—typically within a day for an app in regular use, but longer for an app no one opens.

### Duplicate event suppression

The SDK suppresses duplicate enter and exit events for the same geofence within 1 hour. If a device crosses into a geofence, out again, and then crosses back into the geofence within the next hour, the SDK drops the second entrance event.

## Limitations

*   The SDK doesn’t continuously track the profile’s location.
*   Geofencing depends on operating-system callbacks.
*   Background delivery depends on platform permissions.
*   Android geofencing requires Google Play Services.
*   Mobile operating systems limit how many geofences an app can monitor at once. If your app uses geofencing from more than one source (for example, Customer.io and another SDK), all monitored geofences share the same operating-system limit. The Customer.io SDK manages its own geofences within that limit, but it can’t control geofences registered by other SDKs or by your app.

---

## Tracking > Identify

**Source:** /integrations/sdk/android/tracking/identify

# Identify profiles

You need to identify a user before you can send them messages or track the things they do in your app.

## Identify a profile

Identifying a person:

1.  Adds or updates the person in your workspace. This is basically the same as an [`identify` call to our server-side API](/integrations/api/track/tag/track-customers/identify/).
2.  Saves the person's information on the device. Future calls to the SDK reference the identified person. For example, after you identify a person, any events that you track are automatically associated with that person.
3.  Associates the current device token with the person.

You can only identify one customer at a time. The SDK "remembers" the most recently-identified customer. If you identify person A, and then call the identify function for person B, the SDK "forgets" person A and assumes that person B is the current app user. You can also [stop identifying a person](#clearIdentify), which you might do when someone logs off or stops using your app for a significant period of time.

An identify request takes the following parameters:

*   **userId** (required): The unique value representing a profile—an ID or email address that represents a profile in Customer.io (and your downstream destinations). If you treat `phone` as an identifier, you should identify profiles by `id` or `email` and send the phone number as a `phone` attribute.
*   **traits** (Optional): Contains [attributes](/journeys/people/manage/attributes/) that you want to set for a profile. The `traits` object accepts strings, enums, primitives (int, float, char, etc.), their boxed counterparts (Integer, Float, Character, etc.), arrays, collections, lists, sets, and maps.

We also offer a Kotlin serialization library that can help make it easier to set keys and values for the `traits` object.

```
CustomerIO.instance()
  .identify(
    userId = "USER_ID",
    traits = mapOf("first_name" to "firstName")
  )
```

### Update a profile’s attributes

You store information about a profile in Customer.io as [attributes](/journeys/people/manage/attributes/). When you call the `identify()` function, you can update a profile’s attributes on the server-side.

If a profile is already identified, and the user updates their preferences, provides additional information about themselves, or performs other attribute-changing actions, you can update their profile attributes with `profileAttributes`.

```
CustomerIO.instance().setProfileAttributes(mapOf("favorite_food" to "pizza"))
```

You only need to pass the attributes that you want to set. For example, if you identify a new profile with the attribute `"first_name": "Dana"`, and then you call `CustomerIO.instance().setProfileAttributes(mapOf("favorite_food" to "pizza"))`, the profile’s `first_name` attribute will still be `Dana`. And Dana will now have a `favorite_food` attribute with the value `pizza`.

### Device attributes

By default (if you don’t set `.autoTrackDeviceAttributes(false)` in your config), the SDK automatically collects a series of [attributes](/journeys/people/manage/attributes/) for each device. You can use these attributes in [segments](/journeys/segmentation/data-driven-segments/) and other automation workflow conditions to target the device owner, just like you would use a profile’s other attributes. You cannot, however, use device attributes to personalize messages with [liquid](/journeys/liquid/using-liquid) yet.

Along with these attributes, we automatically set a `last_used` timestamp for each device indicating when the device owner was last identified, and the `last_status` of a push notification you sent to the device. You can also set your own custom device attributes. You’ll see a profile’s devices and each device’s attributes when you go to **Journeys > Profiles > Select a profile**, and click **Devices**.

 ![device attributes on a profile](https://customer.io/images/device-attributes.png)

Your integration shows device attributes in the `context` object

When you inspect calls from the SDK (in your integration’s data in tab), you’ll see device information in the `context` object. We flatten the device attributes that you send into your workspace, so that they’re easier to use in [segments](/journeys/segmentation/segments/). For example, `context.network.cellular` becomes `network_cellular`.

*   idstringrequired
    
    The device token.
    
*   last\_usedinteger(unix timestamp)
    
    The `timestamp` when you last identified this device. If you don't pass a timestamp when you add or update a device, we use the time of the request itself. Our SDKs identify a device when a person launches their app.
    
*   platformstringrequired
    
    The device/messaging platform.
    
    Accepted values: `ios`, `android`
    
*   attributesobject
    
    Attributes that you can reference to segment your audience—like a person's attributes, but specific to a device. These can be either the attributes defined below or custom key-value attributes.
    
    *   device\_osstring
        
        The operating system, including the version, on the device.
        
    *   device\_modelstring
        
        The model of the device a person uses.
        
    *   app\_versionstring
        
        The version of your app that a customer uses. You might target app versions to let people know when they need to update, or expose them to new features when they do.
        
    *   app\_identifierstring
        
        The app that the device belongs to—the **Bundle ID / App identifier** you entered with that app's push credentials. If you have more than one app, we use this attribute to send push notifications to the right app. A device without an app identifier receives push notifications you send to your [default app](/messaging/channels/push/getting-started/#the-default-app).
        
    *   cio\_sdk\_versionstring
        
        The version of the Customer.io SDK in the app.
        
    *   \_last\_statusstring
        
        The delivery status of the last message sent to the device—sent, bounced, or suppressed. An empty string indicates that that the device hasn't received a push yet.
        
        Accepted values: , `bounced`, `sent`, `suppressed`
        
    *   device\_localestring
        
        The device's [IETF language code](/journeys/channels/localization/getting-started/#supported-languages), such as `en-MX` or `es-ES`.
        
    *   push\_enabledstring
        
        If `"true"`, the device is opted-in and can receive push notifications.
        
        Accepted values: `true`, `false`
        
    *   network\_bluetoothboolean
        
        If `true`, the device's bluetooth connection is on.
        
    *   network\_cellularboolean
        
        If `true`, the device's cellular connection is on.
        
    *   network\_wifiboolean
        
        If `true`, the device's WiFi connection is on.
        
    *   screen\_heightinteger
        
        The height of the device's screen in pixels.
        
    *   screen\_widthinteger
        
        The width of the device's screen in pixels.
        
    *   timezonestring
        
        The time zone of the device.
        
    *   Custom Device Attributes \*string
        
        Custom properties that you want to associate with the device.
        

#### Custom device attributes

When we collect device attributes, you can also set custom device attributes with the `deviceAttributes` method. You might do this to save app preferences, time zone, or other custom values specific to the device.

```
CustomerIO.instance().setDeviceAttributes(mapOf("key" to "value"))
```

However, before you set custom device attributes, consider whether the attribute is specific to the `device` or if it applies to a profile more broadly. If you want an attribute to persist beyond the life of the device, you should [apply it to the profile](#identify-again) rather than the device.

#### Disable automatic device attribute collection

By default, the SDK automatically collects the device attributes [defined above](#device-attributes). You can change your config to prevent the SDK from automatically collecting these attributes.

```
// set before you build
builder.autoTrackDeviceAttributes(false)
```

## Stop identifying a profile

When a person logs out, or does something else to tell you that they no longer want to be tracked, you should stop identifying them.

Use `clearIdentify()` to stop identifying the previously identified profile (if there was one).

```
// Future calls to the SDK are anonymous
CustomerIO.instance().clearIdentify()
```

### Identify a different profile

If you want to identify a new profile—like when someone switches profiles on a streaming app, etc—you can simply call `identify()` for the new profile. The new profile then becomes the currently-identified profile, with whom all new information—messages, events, etc—is associated.

---

## Tracking > Lifecycle events

**Source:** /integrations/sdk/android/tracking/lifecycle-events

# Mobile lifecycle events

By default, our Android SDK automatically tracks events that represent the lifecycle of your app and your users experiences with it.

By default, we track the following lifecycle events:

*   **Application Installed**: A user installed your app.
*   **Application Updated**: A user updated your app.
*   **Application Opened**: A user opened your app.
*   **Application Foregrounded**: A user switched back to your app.
*   **Application Backgrounded**: A user backgrounded your app or switched to another app.

You might also want to send your own lifecycle events, like `Application Crashed` or `Application Updated`. You can do this using the `track` method. You’ll find a list of properties for these events—both the ones we track automatically and other events you might send yourself—in our [Mobile App Lifecycle Event specification](/integrations/api/cdp/mobile-app/).

## Lifecycle event examples

A lifecycle event is basically a `track` call that the SDK makes automatically for you.

When you look at your data in Customer.io, you’ll see lifecycle events as `track` calls, where the event `properties` are specific to the name of the event. For example, the `Application Installed` event includes the app `version` and `build` properties.

```
{
  "userId": "app.installer@example.com",
  "type": "track",
  "event": "Application Installed",
  "properties": {
    "version": "3.2.1", "build": "247"
  }
}
```

## Sending custom lifecycle events

You can send your own lifecycle events using the `track` call. However, whenever you send lifecycle events, you should use the *Application EventName* convention that we use for our default lifecycle events.

These [semantic event](/integrations/data-in/semantic-events/) names and properties represent [a standard](/integrations/data-in/semantic-events/mobile-app/) that we use across Customer.io and our downstream destinations. Adhering to this standard ensures that your events automatically map to the correct event types in Customer.io and any other services you send your data to.

If you opt out of automatic lifecycle events, you can send your own `track` calls for these events. Or, for events we can’t track automatically, you might be able to use a webhook or a callback to collect crash events. For example, you might want to send a `track` call for `Application Crashed` when your app crashes or `Application Updated` when people update your app.

```

CustomerIO.instance().track(
  name = "Application Crashed",
  properties = mapOf("url" to "/page/in/app")
)
```

## Disable lifecycle events

We track lifecycle events by default. You can disable this behavior by passing the `setTrackApplicationLifecycleEvents` option to the SDK’s config builder.

```
val builder = CustomerIOConfigBuilder(
    applicationContext = this,
    cdpApiKey = "your-cdp-api-key"
).trackApplicationLifecycleEvents(false)

CustomerIO.initialize(builder.build())
```

---

## Tracking > Location

**Source:** /integrations/sdk/android/tracking/location

# Location tracking

## How it works

The Location module captures location (with user consent) from your app and attaches it to a profile in Customer.io. You can use this data for geo-aware messaging and audience segmentation with more accuracy than [IP-based geolocation](/messaging/profiles/geolocation/).

When you identify a profile, the SDK includes the latest location in the identify call. The SDK also sends a `Location Update` event to the profile’s activity timeline, which you can use in journeys and segments. To balance location updates with battery and data usage, the SDK limits location updates once a day (at most)—and only sends that update when the person has moved a meaningful distance since the last update.

The SDK does not request location permission on its own—your app must handle the permission flow.

## Install the location module

To use location tracking, add the `location` dependency to your app’s `build.gradle` file.

```
implementation 'io.customer.android:location:<version-here>'
```

## Initialize the SDK with the location module

Add `ModuleLocation` when you initialize the SDK. The module takes a `LocationModuleConfig` where you set the tracking mode.

Option

Type

Default

Description

`trackingMode`

`LocationTrackingMode`

`MANUAL`

Controls how and when the SDK captures location. See [tracking modes](#tracking-modes) below.

### Tracking modes

Mode

Description

`MANUAL`

Your app controls when it caputres location. Call `setLastKnownLocation()` or `requestLocationUpdate()` to provide location. You should use this option when your app already has a location-tracking mechanism or you want full control over when you capture location data.

`ON_APP_START`

The SDK automatically captures a one-shot location once per app launch when your app enters the foreground. You can still call `setLastKnownLocation()` or `requestLocationUpdate()` alongside automatic capture. Use this for hands-off location tracking with minimal battery impact.

`OFF`

Disables location tracking entirely. All location calls become silent and location is not included in identify calls. Use this if you want to register the module but disable it at runtime.

KotlinJava

#### Kotlin

```
val config = CustomerIOConfigBuilder(applicationContext, "your-cdp-api-key")
    .addCustomerIOModule(
        ModuleLocation(
            LocationModuleConfig.Builder()
                .setLocationTrackingMode(LocationTrackingMode.MANUAL)
                .build()
        )
    )
    .build()

CustomerIO.initialize(config)
```

#### Java

```
CustomerIOConfigBuilder builder = new CustomerIOConfigBuilder(application, "your-cdp-api-key");
builder.addCustomerIOModule(
    new ModuleLocation(
        new LocationModuleConfig.Builder()
            .setLocationTrackingMode(LocationTrackingMode.MANUAL)
            .build()
    )
);

CustomerIO.initialize(builder.build());
```

## Location APIs

The module provides two methods to capture location. You can call either method as often as you like; the SDK always caches the latest coordinates for profile enrichment, but sends a `Location Update` event no more than once a day—and only if the person has moved a meaningful distance. No matter how frequently you call these methods, the SDK throttles the updates for you so as not to overwhelm your workspace with profile updates.

### `setLastKnownLocation`

Pass coordinates directly from your app’s own location system. This doesn’t require any location permissions from the SDK. Your app manages location access independently of Customer.io.

Parameter

Type

Description

`latitude`

`Double`

Latitude in degrees. Must be between -90 and 90.

`longitude`

`Double`

Longitude in degrees. Must be between -180 and 180.

You can also pass an Android `Location` object directly.

KotlinJava

#### Kotlin

```
// From coordinates
ModuleLocation.instance().locationServices.setLastKnownLocation(37.7749, -122.4194)

// From an Android Location object
ModuleLocation.instance().locationServices.setLastKnownLocation(androidLocation)
```

#### Java

```
// From coordinates
ModuleLocation.instance().getLocationServices().setLastKnownLocation(37.7749, -122.4194);

// From an Android Location object
ModuleLocation.instance().getLocationServices().setLastKnownLocation(androidLocation);
```

### `requestLocationUpdate`

Request a one-shot location from the SDK using Google’s Fused Location Provider. Use this if your app doesn’t have its own location system. Your app must request location permission before calling this method—the SDK won’t prompt the user.

If a user doesn’t grant permission or location services are disabled, the request is ignored—no crash or exception. If a request is already in progress, additional calls are ignored until the current request completes.

1.  Add the permission to your `AndroidManifest.xml`:
    
    ```
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <!-- Optional: for more precise location -->
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    ```
    
2.  Request permission at runtime and call the SDK:
    
    KotlinJava
    
    #### Kotlin
    
    ```
    val launcher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        if (granted) {
            ModuleLocation.instance().locationServices.requestLocationUpdate()
        }
    }
    
    launcher.launch(Manifest.permission.ACCESS_COARSE_LOCATION)
    ```
    
    #### Java
    
    ```
    ActivityResultLauncher<String> launcher = registerForActivityResult(
        new ActivityResultContracts.RequestPermission(),
        granted -> {
            if (granted) {
                ModuleLocation.instance().getLocationServices().requestLocationUpdate();
            }
        }
    );
    
    launcher.launch(Manifest.permission.ACCESS_COARSE_LOCATION);
    ```
    

## Profile switch behavior

When you call `clearIdentify()`, the SDK clears cached location data so that one profile’s location doesn’t carry over to another profile. The next profile you identify starts with a clean slate.

Location persists across app restarts. When your app relaunches, the SDK restores the cached location so that the next `identify()` call includes it automatically.

---

## Tracking > Screen events

**Source:** /integrations/sdk/android/tracking/screen-events

# Screen tracking

Screen views are events that record the pages that your audience visits in your app. They have a `type` property set to `screen`, and a `name` representing the title of the screen or page that a profile visited in your app.

Screen view events let you trigger [automations](/journeys/send/campaigns/overview/) or add profiles to [segments](/journeys/segmentation/data-driven-segments/) based on the parts of your app your audience uses. Screen view events also update your audience’s “Last Visited” attribute, which can help you track how recently profiles used your app.

## Enable automatic screen tracking

When you enable automatic screen tracking, the SDK sends an event every time a person visits a screen in your app. You can turn on automatic screen tracking by appending `autoTrackActivityScreens(true)` to `CustomerIOBuilder`.

When automatically tracking screen events, we capture the name of the screen with the following priority from highest to lowest:

1.  We check if the current `Activity` has a `label` in the manifest file. If it does, the SDK will use the value for `label`.
2.  We get the class name of the `Activity` and use that value.

The SDK will take whatever value it receives and will strip the word `Activity` from it. Example: If you have an `Activity` with the manifest `label` *or* class name `ProfileActivity`, the SDK will track the screen view with the name `Profile`.

```
val builder = CustomerIOConfigBuilder(
    applicationContext = this,
    cdpApiKey = "your-cdp-api-key",
).autoTrackActivityScreens(true)

CustomerIO.initialize(builder.build())
```

If you want to send more data with screen events, or you don’t want to send events for every individual screen that people view in your app, you can [send screen events manually](#manual-screenview).

## Manually track screen events

Screen events use the `.screen` method. Like other events, you can add a map of `properties` object containing additional information about the `screen` event or the currently-identified profile.

```
CustomerIO.instance().screen(
  name = "baseballDailyScores",
  properties = mapOf("prevScreen" to "homescreen", "secondsInApp" to 120)
)
```

## ScreenView Settings

Customer.io uses `screen` events to determine where users are in your app so you can target them with in-app messages on specific screens. By default, the SDK sends `screen` events to Customer.io’s backend servers. But, if you don’t use `screen` events to track user activity, segment your audience, or to trigger automations, these events might constitute unnecessary traffic and event history.

If you don’t use `screen` events for anything other than in-app notifications, you can set the `ScreenViewUse` parameter to `ScreenView.InApp`. This setting stops the SDK from sending `screen` events back to Customer.io but still allows the SDK to use `screen` events for in-app messages, so you can target in-app messages to the right screen(s) without sending event traffic into Customer.io!

```
val builder = CustomerIOConfigBuilder(
    applicationContext = this,
    cdpApiKey = "your_cdp_api_key"
).region(Region.US)
    .screenViewUse(ScreenView.InApp)
    .addCustomerIOModule(
        ModuleMessagingInApp(
            config = MessagingInAppModuleConfig.Builder(
                siteId = "your_site_id",
                region = Region.US
            ).setEventListener(InAppMessageEventListener()).build()
        )
    )
    .addCustomerIOModule(ModuleMessagingPushFCM())

CustomerIO.initialize(builder.build())
```

---

## Tracking > Track events

**Source:** /integrations/sdk/android/tracking/track-events

# Track events

Events represent things people do in your app so that you can track your audience's activity and metrics. Use events to segment your audience, trigger automations, and capture usage metrics in your app.

## Track a custom event

The `track` method helps you send events representing your audience’s activities to Customer.io. When you send events, you can include event `properties`—information about the profile or the event that they performed.

In Customer.io, you can use events to trigger automations and broadcasts. Those automations might send someone a push notification or manipulate information associated with the profile in your workspace.

Events include the following:

*   `name`: the name of the event. Most event-based searches in Customer.io hinge on the name, so make sure that you provide an event name that will make sense to other members of your team.
*   `properties` (Optional): Additional information that you might want to reference in a message. You can reference data attributes in messages and other workflow actions using [liquid](/journeys/liquid/using-liquid) in the format `{{event.<attribute>}}`.

```
CustomerIO.instance().track(
  name = "purchase",
  properties = mapOf("product" to "socks", "price" to "4.99")
)
```

Perform downstream actions with semantic events

Some downstream actions don’t neatly map to our simple `identify`, `track`, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations. See [Semantic Events](#semantic-events) for more information.

### Anonymous activity

If you send a `track` call before you `identify` a profile, we’ll attribute the event to an `anonymousId`. When you identify the profile, we’ll reconcile their anonymous activity with the identified profile.

When we apply anonymous events to an identified profile, the previously anonymous activity becomes eligible to trigger automations in Customer.io.

## Semantic Events

Some actions don’t map cleanly to our simple `identify`, `track`, and other calls. For these, we use “semantic events,” events that have a special meaning in Customer.io and your destinations.

These are especially important in Customer.io for destructive operations like deleting a profile. When you send an event with a semantic `event` name, we’ll perform the appropriate action.

For example, if a person decides to leave your service, you might delete them from your workspace. In Customer.io, you’ll do that with a `Delete Person` event.

For example, you can associate a profile with a group (called an [object](/journeys/objects-data/objects/) in Customer.io) with a simple `group` call. But what if you wanted to remove someone from the group? In Customer.io, you do that with an event called `Relationship Deleted`.

```
CustomerIO.instance().track(
  name = "User Deleted
)
```

---

## Whats new > 3.x upgrade

**Source:** /integrations/sdk/android/whats-new/3.x-upgrade

# 2.x -> 3.x

This page details breaking changes from previous versions, so you understand the development effort required to update your app and take advantage of the latest features.

## Versioning

We try to limit breaking or significant changes to major version increments. The three digits in our versioning scheme represent major, minor, and patch increments respectively.

 ![sdk versioning scheme](https://customer.io/images/sdk-versions.jpg)

*   **Major**: may include breaking changes, and generally introduces significant feature updates.
*   **Minor**: may include new features and fixes, but won’t include breaking changes. You may still need to do some development to use new features in your app.
*   **Patch**: Increments represent minor fixes that should not require development effort.

## Upgrade from 2.x to 3.x

Android 12 changes the way the operating system resolves deep links. We’ve resolved the issue in our 3.x release, involving the following behavioral changes.

If your app is open or you send a data notification with a non-app link:

*   Android 12 or later: Your notification will launch the host app first, and then a matching app on top of it.
*   Android 11 or earlier: Your notification will launch the matching app without launching the host app.

By default, you You can now disable the ability to open links outside the app from your SDK configuration.

### `CustomerIOPushNotificationCallback` replaces `CustomerIOUrlHandler`

The new `CustomerIOPushNotificationCallback` class handles deep links with Android 12 or later that otherwise would not have worked with the previous `CustomerIOUrlHandler` class.

The `ModuleMessagingPushFCM` now has a config object that contains this `notificationCallback` and an optional `redirectDeepLinksToOtherApps` boolean (defaults to `true`).

```
class MainApplication : Application(), CustomerIOPushNotificationCallback {

    override fun onCreate() {
        super.onCreate()
        val builder = CustomerIOBuilder(
            siteId = "YOUR-SITE-ID",
            apiKey = "YOUR-API-KEY",
            appContext = this
        )
        builder.addCustomerIOModule(
            ModuleMessagingPushFCM(
                    config = MessagingPushModuleConfig.Builder().apply {
                        setNotificationCallback(this)
                        setRedirectDeepLinksToOtherApps(false)
                    }.build()
                )
        )
        builder.build()
    }

    override fun createTaskStackFromPayload(
        context: Context,
        payload: CustomerIOParsedPushPayload
    ): TaskStackBuilder? {
        // return TaskStackBuilder of your choice if you plan to handle the deep link yourself
        // return null if you want CustomerIO SDK to do it for you
        TODO("Pass the link to your Deep link managers")
    }
}
```

## Upgrade from 1.x to 2.x

### Remove `.enqueue()`

The Android SDK 2.0 release introduces [a queue system](/integrations/sdk/android/getting-started/#the-processing-queue), making it easier to integrate with the SDK.

All of the SDK functions that previously required a `.enqueue()` call, no longer do. Simply delete that code in your app to migrate to using the queue.

```
// Before
CustomerIO.instance().track(...).enqueue {...}
// After
CustomerIO.instance().track(...)
```

This impacts the following functions:

*   `CustomerIO.instance().identify(...)`
*   `CustomerIO.instance().track(...)`
*   `CustomerIO.instance().screen(...)`
*   `CustomerIO.instance().registerDeviceToken(...)`
*   `CustomerIO.instance().deleteDeviceToken(...)`
*   `CustomerIO.instance().trackMetric(...)`

If you use the optional [FCM Push notification SDK](/integrations/sdk/android/getting-started/#install), you’ll also benefit from the queue.

```
// Before
CustomerIOFirebaseMessagingService.onMessageReceived(context, remoteMessage, errorCallback = { ... })
// After 
CustomerIOFirebaseMessagingService.onMessageReceived(context, remoteMessage)

// Before
CustomerIOFirebaseMessagingService.onNewToken(token) { ... }
// After 
CustomerIOFirebaseMessagingService.onNewToken(token)
```

### Initialize optional SDKs

To keep your app size as small as possible, the Customer.io SDK is broken up into optional SDKs that you install only when you need them. In version 1.0, you only needed to install a dependency with Gradle to use an optional SDK.

Version 2.0 introduces a breaking change that requires you to *initialize* optional SDKs after you install them via Gradle.

For example, if you have the optional [FCM Push notifications SDK](/integrations/sdk/android/getting-started/#install) installed, you need to add 1 new line to the `CustomerIOBuilder`:

```
// Before 
CustomerIOBuilder()
.build()

// After
CustomerIOBuilder()
.addCustomerIOModule(ModuleMessagingPushFCM())
.build()
```

The [FCM Push notifications SDK](/integrations/sdk/android/getting-started/#install) is the currently only optional SDK module. When we release additional SDKs in the future, you can expect to initialize these SDKs as well.

---

## Whats new > 4.10 upgrade

**Source:** /integrations/sdk/android/whats-new/4.10-upgrade

# Upgrade from 4x to 4.10

This page details changes from the previous major version of the SDK to this minor update, so you understand the development effort required to update your app and take advantage of the latest features.

## What changed?

The changes in this update are mainly to align our APIs across different platforms. You should not experience changes to functionality or features.

## Changes to initialization

`CustomerIOBuilder` is deprecated. You should use `CustomerIOConfigBuilder` instead.

#### Before

```
CustomerIOBuilder(
applicationContext = this,
// new credentials
cdpApiKey = "your_cdp_api_key"
migrationSiteId = "your_site_id"
).apply {
// If you're in the EU, set Region.EU
region(Region.US)
addCustomerIOModule(
  ModuleMessagingInApp(
    // the in-app module now has its own configuration
     config = MessagingInAppModuleConfig.Builder(
        siteId = "your_site_id",
        region = Region.US
     ).setEventListener(InAppMessageEventListener()).build()
  )
)
addCustomerIOModule(ModuleMessagingPushFCM())
build()
}
```

#### After

```
val builder = CustomerIOConfigBuilder(
applicationContext = this,
cdpApiKey = "your_cdp_api_key",
).region(Region.US)
// Add migrationSiteId only if you had it before
.migrationSiteId("your_site_id")
.addCustomerIOModule(
    ModuleMessagingInApp(
        // the in-app module now has its own configuration
        config = MessagingInAppModuleConfig.Builder(
            siteId = "your_site_id",
            region = Region.US
        ).setEventListener(InAppMessageEventListener()).build()
    )
)
.addCustomerIOModule(ModuleMessagingPushFCM())

CustomerIO.initialize(builder.build())
```

## Changes when you track users, events, and screens

You’ll need to update the way you identify users and track events, including screen tracking.

### Identifying a user

These variants of `identify` are **deprecated**:

*   `identify(userId: String, traits: T)` where traits are a generic type
*   `identify(userId: String, traits: JsonObject)`
*   `identify(userId: String, traits: Traits, serializationStrategy: SerializationStrategy<Traits>)` where traits are a generic type

**You should use this instead:**

*   `identify(userId: String, traits: Map<String, Any?>)`

```
CustomerIO.instance().identify(userId, mapOf("name" to "John Doe"))
```

### Tracking an event

These variants of `track` are **deprecated**:

*   `track(name: String, properties: T)` where traits are a generic type
*   `track(name: String, properties: JsonObject)`
*   `track(name: String, properties: Traits, serializationStrategy: SerializationStrategy<Traits>)` where traits are a generic type

**You should use this instead:**

*   `track(name: String, properties: Map<String, Any?>)`

```
CustomerIO.instance().track("clicked_button", mapOf("button_name" to "Login"))
```

### Screen tracking

These variants of `screen` are **deprecated**:

*   `screen(title: String, properties: T)` where traits are a generic type
*   `screen(title: String, properties: JsonObject)`
*   `screen(title: String, properties: Traits, serializationStrategy: SerializationStrategy<Traits>)` where traits are a generic type

**You should use this instead:**

*   `screen(title: String, properties: Map<String, Any?>)`

```
CustomerIO.instance().screen("Home", mapOf("login" to true))
```

### Profile and device attribute changes

You’ll need to update the way you set profile and device attributes.

**`profileAttributes` is deprecated**

*   Getter has no replacement, the mobile SDK doesn’t expose the user’s profile attributes
*   Setter is replaced with `setProfileAttributes(attributes: Map<String, Any>)`

```
CustomerIO.instance().setProfileAttributes(mapOf("name" to "John Doe"))
```

**`deviceAttributes` is deprecated**

*   Getter has no replacement, the mobile SDK doesn’t expose the user’s device attributes
*   Setter is replaced with `setDeviceAttributes(attributes: Map<String, Any>)`

```
CustomerIO.instance().setDeviceAttributes(mapOf("device_id" to "1234567890"))
```

---

## Whats new > 4.x upgrade

**Source:** /integrations/sdk/android/whats-new/4.x-upgrade

# Upgrade from 3x to 4x

This page details breaking changes from the previous major version of the SDK, so you understand the development effort required to update your app and take advantage of the latest features.

## What changed?

This update provides native support for our new integrations framework. While this represents a significant change “under the hood,” we’ve tried to make it as seamless as possible for you; much of your implementation remains the same.

This move also adds two additional features:

*   **Support for anonymous tracking**: you can send events and other activity for anonymous users, and we’ll reconcile that activity with a profile when you identify them.
*   **Built-in lifecycle events**: the SDK now automatically captures events like “Application Installed” and “Application Updated” for you.
*   **New device-level data**: the SDK captures the device `name` and other device-level context for you.

*   **A new `group` API call**: this new call helps you relate profiles to [objects](/journeys/objects-data/objects/) in Customer.io and accounts in other, downstream destinations.

When you’re done, you’ll be able to use your app data’s in both Customer.io *and* other downstream destinations—like your analytics platform, data warehouse, or CRM. All that *and* you’ll be prepared to accept new features and improvements that we roll out in the future!

## Upgrade process

You’ll update initialization calls for the SDK itself and the push and/or in-app messaging modules.

**As a part of this process, your credentials change**. You’ll need to set up a new integration in Customer.io and get the *CDP API Key* you’ll use to initialize the SDK. But you’ll *also* need to keep your previous `siteId` as a `migrationSiteId` when you initialize the SDK. The `migrationSiteId` is a key helps the SDK send remaining traffic when people update your app.

When you’re done, you’ll also need to change a few base properties to fit the new APIs. In general, `identifier` becomes `userId`, `body` becomes `traits`, and `data` becomes `properties`.

### 1\. Get your new *CDP API Key*

The new version of the SDK requires you to set up a new integration in Customer.io. As a part of this process, you’ll get your *CDP API Key*.

1.  Go to [*Integrations*](https://fly.customer.io/workspaces/last/journeys/integrations/all/overview) and click **Add Integration**.
2.  Select **Android**.
    
     ![set up your android source](https://customer.io/images/cdp-android-source.png)
    
3.  Enter a *Name* for your integration, like “My Android App”.
4.  We’ll present you with a code sample containing a `cdpApiKey` that you’ll use to initialize the SDK. Copy this key and keep it handy.
5.  Click **Complete Setup** to finish setting up your integration.
    
     ![Set your name, get your CDP API Key, and click Complete Setup](https://customer.io/images/cdp-android-source-setup.png)
    

Now the *Integrations* page shows that your Android integration is connected to your workspace. You can also [connect your Android integration to other services](/integrations/data-out/add-destination/) if you want to send your mobile data to other places outside of Customer.io—like your analytics provider, data warehouse, or CRM.

### 2\. Import datapipelines instead of tracking

We’ve replaced the `tracking` package with `datapipelines`. You’ll need to update your import statements to reflect this change.

```
// replace `tracking` with:
implementation 'io.customer.android:datapipelines:4.20.3'
```

### 3\. Update your initialization

You’ll initialize the new version of the SDK and its packages with `SDKConfigBuilder` objects instead of a `CustomerIOConfig`. A few of the configuration options changed. In particular,

*   `CustomerIOBuilder` replaces `CustomerIO.Builder`.
*   `cdpApiKey` replaces `apiKey`: this is a new key that you got from [Step 1](#1-get-your-new-cdp-api-key)
*   `migrationSiteId` replaces `siteId`: this is the same key you used in the previous version of the SDK. **You need to include this property** to send remaining traffic when people update your app.
*   `AutoTrackActivityScreens` replaces `autoTrackScreenViews`: functionality is unchanged.
*   The `messagingInApp` module now includes a site ID and region—these tell the SDK which workspace your in-app messages come from.

```
CustomerIOBuilder(
   applicationContext = this,
   // new credentials
   cdpApiKey = "your_cdp_api_key"
   migrationSiteId = "your_site_id"
).apply {
  // If you're in the EU, set Region.EU
   region(Region.US)
   addCustomerIOModule(
      ModuleMessagingInApp(
        // the in-app module now has its own configuration
         config = MessagingInAppModuleConfig.Builder(
            siteId = "your_site_id",
            region = Region.US
         ).setEventListener(InAppMessageEventListener()).build()
      )
   )
   addCustomerIOModule(ModuleMessagingPushFCM())
   build()
}
```

### 4\. Update your `identify`, `track`, and `screen` calls

Our APIs changed slightly in this release. We’ve done our best to make the new APIs as similar as possible to the old ones. The names of a few properties that you’ll pass in your calls have changed, but their functionality has not.

*   `identify`: `identifier` becomes `userId` and `body` becomes `traits`
*   `track`: `data` becomes `properties`
*   `screen`: `name` becomes `title`, and `data` becomes `properties`

We’ve highlighted changes in the sample below.

```
//identify: identifier becomes userId, body becomes traits
CustomerIO.instance()
  .identify(
    userId = "USER_ID",
    traits = mapOf("first_name" to "firstName")
  )

// track: data becomes properties
CustomerIO.instance().track(
  name = "purchase",
  properties = mapOf("product" to "socks", "price" to "4.99")
)

// screen: name becomes title, data becomes properties
CustomerIO.instance().screen(
  title = "purchase",
  properties = mapOf("product" to "socks", "price" to "4.99"),
  category: String = "" // optional
)
```

## Configuration Changes

As a part of this release, we’ve changed a few configuration options. The `MessagingInApp` and `MessagingPush` modules also now take their own configuration options.

### `datapipelines` configuration options

For the base SDK, you’ll use `SDKConfigBuilder` to set your configuration options. The following table shows the changes to the configuration options.

Field

Type

Default

Description

`cdpApiKey`

string

Replaces `apiKey`; required to initialize the SDK and send data to Customer.io.

`migrationSiteId`

string

Replaces `siteId`; required if you’re updating from 2.x. This is the key representing your previous version of the SDK.

`AutoTrackActivityScreens`

boolean

`false`

Replaces `autoTrackScreenViews`; functionality is unchanged. We simply renamed the option to reflect support for UIKit and *not* SwiftUI.

`trackApplicationLifeCycleEvents`

boolean

`true`

When true, the SDK automatically tracks application lifecycle events (like *Application Installed*).

### `MessagingInApp` configuration options

When you initialize the `MessagingInApp` package, **you must pass both of these configuration options**.

Option

Type

Default

Description

`siteId`

string

The [Site ID](https://fly.customer.io/env/last/settings/api_credentials) from a set of Track API credentials; this determines the workspace that your app listens for in-app messages from.

`Region`

`.US` or `.EU`

`.US`

The region your Customer.io account resides in—US or EU.

---

## Whats new > Changelog

**Source:** /integrations/sdk/android/whats-new/changelog

# Android Changelog

Check out release history for stable releases of android SDKs. Stable releases have been tested thoroughly and are ready for use in your production apps. Major versions may include breaking changes. See [our migration guide](/integrations/sdk/android/migrate-upgrade) for help updating your SDK integration to take advantage of new features and fixes.

Need to upgrade?

Select your current version to see everything that changed between it and the latest Android release.

Select a version…4.20.34.20.24.20.14.20.04.19.04.18.24.18.14.18.04.17.14.17.04.16.14.16.04.15.24.15.14.15.04.14.04.13.04.12.14.12.04.11.04.10.24.10.14.10.04.9.04.8.04.7.14.7.04.6.34.6.24.6.14.6.04.5.84.5.74.5.64.5.54.5.44.5.34.5.24.5.14.5.04.4.14.4.04.3.04.2.04.1.04.0.14.0.03.11.23.11.13.11.03.10.03.9.23.9.13.9.03.8.23.8.13.8.03.7.13.7.03.6.73.6.63.6.53.6.43.6.33.6.23.6.13.6.03.5.33.5.23.5.13.5.03.4.33.4.23.4.13.4.03.3.23.3.13.3.03.2.03.1.13.1.03.0.02.1.12.1.02.0.12.0.01.0.51.0.41.0.31.0.21.0.11.0.0

## 4.x Releases

* * *

### 4.20.3

September 8, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.20.3)

#### Bug Fixes

*   **deps:** update Faraday security pin to 1.10.6 ([#845](https://github.com/customerio/customerio-android/issues/845)) ([787d310](https://github.com/customerio/customerio-android/commit/787d3100a5289840be6b6c22e2e60409bc47c06b))

### 4.20.2

August 21, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.20.2)

#### Fixes

*   **In-App:** Added multi-app support ([#812](https://github.com/customerio/customerio-android/issues/812)) ([bbd5616](https://github.com/customerio/customerio-android/commit/bbd561674b0c0ad46ae191d20166fda70a5e102e))

### 4.20.1

August 6, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.20.1)

#### Bug Fixes

*   **Geofence:** Exit events no longer fire for geofences the device never entered ([#805](https://github.com/customerio/customerio-android/pull/805)) ([c70ac93](https://github.com/customerio/customerio-android/commit/c70ac931b0a5d09b7954b9ffe89537fd55d09b51))
*   **Geofence:** No more duplicate enter events after a device reboot or app update ([#805](https://github.com/customerio/customerio-android/pull/805)) ([c70ac93](https://github.com/customerio/customerio-android/commit/c70ac931b0a5d09b7954b9ffe89537fd55d09b51))
*   **Geofence:** Geofences the device is currently inside always stay monitored, so their exit events aren't missed ([#806](https://github.com/customerio/customerio-android/pull/806)) ([27c64d6](https://github.com/customerio/customerio-android/commit/27c64d621028541d9534fa09148c56d937ba118a))
*   **Geofence:** Nearby geofences are refreshed when the app returns to the foreground ([#807](https://github.com/customerio/customerio-android/pull/807)) ([7f97fc3](https://github.com/customerio/customerio-android/commit/7f97fc313298801741fb96f31c4d70eab3a4d9eb))
*   **Geofence:** More reliable geofence updates while on the move ([#808](https://github.com/customerio/customerio-android/pull/808)) ([b86f237](https://github.com/customerio/customerio-android/commit/b86f237a71f08aa9511e36b7c4fb33bbba68188e))

### 4.20.0

July 30, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.20.0)

#### Features

*   add support for geofencing feature ([#763](https://github.com/customerio/customerio-android/issues/763)) ([7a1f85b](https://github.com/customerio/customerio-android/commit/7a1f85b734802863f25353eb08b3370978c45eb3))
*   enable Live Notifications ([#734](https://github.com/customerio/customerio-android/issues/734)) ([6409037](https://github.com/customerio/customerio-android/commit/6409037fcc85a169759480be4eb34c6445c09359))
*   Visual Notification Inbox overlay (integration / build distribution) ([#769](https://github.com/customerio/customerio-android/issues/769)) ([e28fa9f](https://github.com/customerio/customerio-android/commit/e28fa9f9653dc2c11c4a215c0ec23c2021bea04e))

### 4.19.0

July 1, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.19.0)

### 4.18.2

June 22, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.18.2)

#### Bug Fixes

*   scope in-app polling to process lifecycle instead of per-activity ([#746](https://github.com/customerio/customerio-android/issues/746)) ([5d30ba1](https://github.com/customerio/customerio-android/commit/5d30ba19f412884ca109539486d14bae9b843e9e))

### 4.18.1

June 15, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.18.1)

#### Bug Fixes

*   harden push notification activity and in-app message WebView ([#733](https://github.com/customerio/customerio-android/issues/733)) ([1323529](https://github.com/customerio/customerio-android/commit/13235298de8a3eb0c9ece2fe2c9eab677cd65689))

### 4.18.0

June 4, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.18.0)

#### Features

*   events volume optimization release ([#715](https://github.com/customerio/customerio-android/issues/715)) ([5324714](https://github.com/customerio/customerio-android/commit/5324714e021708a47f730cb4c7de5630b022d8eb))

### 4.17.1

May 18, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.17.1)

#### Bug Fixes

*   Defined a working range of versions for the Segment Analytics ([#679](https://github.com/customerio/customerio-android/issues/679)) ([865851b](https://github.com/customerio/customerio-android/commit/865851bac927c9063bd1774cb4e4109b3029ec53))

### 4.17.0

March 9, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.17.0)

#### Features

*   Location enrichment ([#666](https://github.com/customerio/customerio-android/issues/666)) ([8d856bc](https://github.com/customerio/customerio-android/commit/8d856bcd811c9d8872041a4baf541272a69edd83))

### 4.16.1

February 24, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.16.1)

#### Bug Fixes

*   in-app messages not displaying when app returns from background with SSE enabled ([#663](https://github.com/customerio/customerio-android/issues/663)) ([9fb8156](https://github.com/customerio/customerio-android/commit/9fb81565dfdc458cf8bcc1baa89a6d1ebdc2272e))

### 4.16.0

February 20, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.16.0)

#### Features

*   Added support for Notification Inbox ([#660](https://github.com/customerio/customerio-android/issues/660)) ([7bc46da](https://github.com/customerio/customerio-android/commit/7bc46daa84bd909f58fe99ce7d70855f6086292a))

### 4.15.2

February 16, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.15.2)

#### Bug Fixes

*   Downgrade OkHttp to v4 ([#653](https://github.com/customerio/customerio-android/issues/653)) ([8d97548](https://github.com/customerio/customerio-android/commit/8d975488d6baac398ae6a6353fc50e1f53e305a0))

### 4.15.1

February 4, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.15.1)

#### Bug Fixes

*   In app improvements ([#640](https://github.com/customerio/customerio-android/issues/640)) ([2270377](https://github.com/customerio/customerio-android/commit/22703777fa43d091600e93a873908b2538185f7e))

### 4.15.0

January 13, 2026 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.15.0)

#### Features

*   In-app messages now support SSE (Server-Sent Events) as an alternative to polling, reducing latency and improving message delivery efficiency ([#636](https://github.com/customerio/customerio-android/issues/636)) ([6cba369](https://github.com/customerio/customerio-android/commit/6cba369610d4fd8c5d4fc8454ff9b7abe6c28421))

### 4.14.0

November 24, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.14.0)

#### Features

*   lead capture for anonymous messages ([#634](https://github.com/customerio/customerio-android/issues/634)) ([8342099](https://github.com/customerio/customerio-android/commit/834209952352d3deb26a8dcee9cd12d1a8413f40))

### 4.13.0

October 29, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.13.0)

#### Features

*   Updated the SDK and dependencies for Android 16 compatibility, including minor updates to better support newer OS restrictions and behavior changes. ([#626](https://github.com/customerio/customerio-android/issues/626)) ([2e36d0b](https://github.com/customerio/customerio-android/commit/2e36d0bc6edd5ad89af3891821a82b9cee8d0383))

#### ⚠️ Notes

*   Apps now may need to update their `compileSdk` version to `36` and Gradle version to at least `8.9.3` to ensure compatibility with updated dependencies and to successfully build against Android 16.

### 4.12.1

October 24, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.12.1)

#### Bug Fixes

*   Fixed a crash in release builds where anonymous messages could fail due to missing `TypeToken` definitions when R8/ProGuard optimizations were applied. ([#623](https://github.com/customerio/customerio-android/issues/623)) ([4fef7a8](https://github.com/customerio/customerio-android/commit/4fef7a8a5f382091d0213a00e2ec1892ba439185))

### 4.12.0

October 8, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.12.0)

#### Features

*   You can now send banners, modals, pop-ups, and surveys to anonymous visitors —no ID or email required.([#614](https://github.com/customerio/customerio-android/issues/614)) ([bfcbb80](https://github.com/customerio/customerio-android/commit/bfcbb806a8af61e7e8f8f1c2998aba85ec154dc0))

### 4.11.0

October 7, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.11.0)

#### Features

*   Improve push notificaiton delivery receipts delay ([#618](https://github.com/customerio/customerio-android/issues/618)) ([ce5cdf3](https://github.com/customerio/customerio-android/commit/ce5cdf370d97825f83843bf163767e02e871c292))

### 4.10.2

October 3, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.10.2)

#### Bug Fixes

*   optimize inline in-app reloading ([#611](https://github.com/customerio/customerio-android/issues/611)) ([a201c86](https://github.com/customerio/customerio-android/commit/a201c86fe78b75f84b8796f8b367de24a65566f9))

### 4.10.1

August 29, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.10.1)

#### Bug Fixes

*   Resolved a crash when dismissing in app messages using back press during initial loading phase. Users can now safely navigate away from messages without encountering unexpected app crashes. ([#608](https://github.com/customerio/customerio-android/issues/608)) ([ea725f0](https://github.com/customerio/customerio-android/commit/ea725f02e39b5aae5b0ca1895795f3d39de20afb))

### 4.10.0

August 21, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.10.0)

#### Features

*   Align public API with other CIO SDK platforms ([#600](https://github.com/customerio/customerio-android/issues/600)) ([fedbcc9](https://github.com/customerio/customerio-android/commit/fedbcc954a254bcbc678f1091098f94d634963db))

### 4.9.0

August 18, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.9.0)

#### Features

*   Added support for queue sticky sessions ([#598](https://github.com/customerio/customerio-android/issues/598)) ([4dd7639](https://github.com/customerio/customerio-android/commit/4dd7639120800d81712c0ed50656e10ed9a65d45))

### 4.8.0

August 11, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.8.0)

#### Breaking Features

*   Support for Kotlin 2+ is added ([#591](https://github.com/customerio/customerio-android/issues/591)) ([b5f94ff](https://github.com/customerio/customerio-android/commit/b5f94ff89f487dd32db07bef9d5b4f528fa77af5))

but this also means apps needs to have the following minimum requirement

*   Gradle: 8.0 or later
*   Android Gradle Plugin (AGP): 8.0 or later (8.2+ recommended)
*   Kotlin: 1.9.20 or later (2.0+ required if using Kotlin Multiplatform or K2-specific features)

### 4.7.1

July 22, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.7.1)

#### Bug Fixes

*   Fixes a crash when build attributes from device are nullable ([#584](https://github.com/customerio/customerio-android/issues/584)) ([33a938f](https://github.com/customerio/customerio-android/commit/33a938f030b2bb4883fa5a391b1511135aebf0a0))

### 4.7.0

July 16, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.7.0)

#### Features

*   Add ability to configure messaging channels for local notifications ([#582](https://github.com/customerio/customerio-android/issues/582)) ([75ac690](https://github.com/customerio/customerio-android/commit/75ac6900c21bf97e05f1808cb7524ecb4437ca5e))

### 4.6.3

June 25, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.6.3)

#### Internal

*   Added base classes around Inline views to support communication of inline view with Wrapper SDKs ([#579](https://github.com/customerio/customerio-android/issues/579)) ([1459529](https://github.com/customerio/customerio-android/commit/145952954532ff65ed6e4ecd93f01c93c15b25fc))

### 4.6.2

June 25, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.6.2)

#### Bug Fixes

*   Sanitize unsupported numeric values, this fixes a crash when customers attempt to pass values that are not supported by JSON as properties for event or screen tracking (ex: `NaN` and +-`Inf`) ([#569](https://github.com/customerio/customerio-android/issues/569)) ([e75a721](https://github.com/customerio/customerio-android/commit/e75a721239ff17624f963760157766a8b88d9ae6))

### 4.6.1

May 29, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.6.1)

#### Bug Fixes

*   Fixed a crash that occurred only when the app was restored by Android OS from background after being removed from memory with a modal in-app message active. The issue primarily affected wrapper SDKs or apps that don't initialize the SDK in `Application` class at startup. ([#561](https://github.com/customerio/customerio-android/issues/561)) ([83003b5](https://github.com/customerio/customerio-android/commit/83003b53e38fdc74cc608715112d65fb502ec802))

### 4.6.0

May 23, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.6.0)

#### Features

*   Added support for inline in-app messages. Inline in-app messages act like a part of the content on your page. They let you dynamically populate parts of your app and talk to your customers without interrupting their experience. ([#557](https://github.com/customerio/customerio-android/issues/557)) ([854a8ad](https://github.com/customerio/customerio-android/commit/854a8ad0445953768de9b15964a482ee45ae1463))

### 4.5.8

April 24, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.5.8)

#### Bug Fixes

*   Use SDK for tracking events in plugin to follow same lifecycle as other events ([#521](https://github.com/customerio/customerio-android/issues/521)) ([1c0b7a6](https://github.com/customerio/customerio-android/commit/1c0b7a6c7bc3b14820b9c6bab3168025206cbfbd))

### 4.5.7

April 18, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.5.7)

#### Bug Fixes

*   Fixed an issue where, upon FCM token refresh, the newly generated token was mistakenly deleted and re-added. With this fix, only the old unused token is deleted, and the new token is retained and added as expected. ([#517](https://github.com/customerio/customerio-android/issues/517)) ([ccb9de2](https://github.com/customerio/customerio-android/commit/ccb9de27e7ad38e2569a5a9d91d9fec2cd5293ce))

### 4.5.6

April 11, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.5.6)

#### Bug Fixes

*   Added support for null handling in nested structures in attributes/traits ([#508](https://github.com/customerio/customerio-android/issues/508)) ([bf6cb1b](https://github.com/customerio/customerio-android/commit/bf6cb1bdf62b43e4dbaade46efaca449ea296f4e))

### 4.5.5

April 1, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.5.5)

#### Bug Fixes

*   Fixed the sequencing of screen tracking events for in-app messaging current screen state ([#500](https://github.com/customerio/customerio-android/issues/500)) ([6877daf](https://github.com/customerio/customerio-android/commit/6877daf98235ce9c96a2ce4932f188efb2c33a71))

### 4.5.4

April 1, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.5.4)

#### Bug Fixes

*   Resolved syncing issues for events stored while in battery saver (offline) mode ([#498](https://github.com/customerio/customerio-android/issues/498)) ([6f3d16f](https://github.com/customerio/customerio-android/commit/6f3d16fe01a675cfa522099230baf03650cf9c42))

### 4.5.3

February 18, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.5.3)

#### Bug Fixes

*   Android 14 introduced strict rules for when apps are in the killed state, impacting push delivery tracking. This release fixes that. ([#493](https://github.com/customerio/customerio-android/issues/493)) ([ea5c7a7](https://github.com/customerio/customerio-android/commit/ea5c7a780e3c4cfd5160faf69cf0e5bd2a99fd23))

### 4.5.2

January 8, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.5.2)

#### Bug Fixes

*   Fixes in-app messages overlay background color being ignored from in-app message payload ([#485](https://github.com/customerio/customerio-android/issues/485))

### 4.5.1

January 7, 2025 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.5.1)

#### Bug Fixes

*   Fixes the bug where device update/delete events would not migrate automatically when migrating from v3 to v4 ([#481](https://github.com/customerio/customerio-android/issues/481))

### 4.5.0

December 17, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.5.0)

#### Features

*   Added ability to disable forwarding screen events to destinations/servers. Apps can still send screen events for local processing and use them for page rules in in-app messages by just updating SDK configuration during initialization. Learn more about [screen configuration options here](https://docs.customer.io/sdk/android/getting-started/packages-options/#configuration-options). ([#474](https://github.com/customerio/customerio-android/issues/474)) ([b5ea8e9](https://github.com/customerio/customerio-android/commit/b5ea8e91927c4e6a9aed8530227190d4eeaca7cc))

#### Fixes

*   Handle the case where the user identifier would be set to an empty string, if profile attributes were updated before identifying the profile. #476

### 4.4.1

November 29, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.4.1)

#### Bug Fixes

*   Fixes issue with loading legacy editor messages ([#469](https://github.com/customerio/customerio-android/issues/469)) ([ecd89dd](https://github.com/customerio/customerio-android/commit/ecd89ddbeb0abe9f6bac8704ee6be66410c0837c))

### 4.4.0

November 13, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.4.0)

##### Features

*   This release introduces support for displaying larger in-app messages. ([#456](https://github.com/customerio/customerio-android/issues/456)) ([b8331bf](https://github.com/customerio/customerio-android/commit/b8331bf7cea27628ebbb4d3070a897e1073c789f))

##### Internal

*   Updated our SDK to use the v2 version of our in-app messages API. This will provide a more reliable experience for in-app messages. ([#461](https://github.com/customerio/customerio-android/issues/461))

#### *Note*:

This is only supported for the new in-app editor, if you are using the legacy in-app editor, it is recommended *NOT* to upgrade to this release for now.

### 4.3.0

October 16, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.3.0)

#### Improvement

*   This release includes changes needed to support for data pipelines in the our React Native SDK. There are no new features for customers utilizing our native Android SDK only.

### 4.2.0

September 11, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.2.0)

#### Features

*   Revamped the in-app messaging module to enhance performance by optimizing local processing, accelerating message loading during page transitions, and implementing a state-driven architecture for better responsiveness and control. ([#431](https://github.com/customerio/customerio-android/issues/431)) ([682ac42](https://github.com/customerio/customerio-android/commit/682ac4221f2e118599fe75129c09f6595af5a9ff))
*   We highly recommend upgrading to the new in-app editor to ensure optimal performance.

#### Fixes

*   We reduced the chance of duplicate in-app messages by keeping a local store of messages already displayed.
*   Resolves an issue with tracking attributes for certain in-app messages.
*   Fixes an intermittent crash on initialization. [#426](https://github.com/customerio/customerio-android/issues/426)
*   Resolves an application not responding (ANR) issue. [#147](https://github.com/customerio/customerio-flutter/issues/147)

### 4.1.0

August 9, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.1.0)

#### Features

*   Ability to dismiss current in-app message. Currently displayed message can now be dismissed using `CustomerIO.instance().dismissMessage()` ([#420](https://github.com/customerio/customerio-android/issues/420)) ([8bfc37d](https://github.com/customerio/customerio-android/commit/8bfc37dea3cbf96c5508f8aeb7f8ebe112db12ce))
*   Improved logs around in-app messaging to provide more clarity on state changes and actions performed on in-app messages ([#421](https://github.com/customerio/customerio-android/issues/421)) ([8bfc37d](https://github.com/customerio/customerio-android/commit/3ae7655b93c4c5b489b3dc65fd5f33a41cffd41e))

### 4.0.1

July 25, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.0.1)

#### Bug Fixes

*   Resolves an issue that caused the SDK version to be reported as the previous version for versions 3.3.0 - 4.0.0. ([#415](https://github.com/customerio/customerio-android/issues/415)) ([1358719](https://github.com/customerio/customerio-android/commit/13587190e45d0b25c97846dc41d9cbc91e8bce48))

### 4.0.0

July 24, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/4.0.0)

#### ⚠ BREAKING CHANGES

*   Data Pipelines Support: Sending your mobile data into our customer data platform (CDP) helps you support both Journeys and the rest of your martech stack—analytics, data warehouses, CRMs and more. ([#412](https://github.com/customerio/customerio-android/issues/412)) ([772b489](https://github.com/customerio/customerio-android/commit/772b4895a5d0599f6b4b375e925a012545d70cc3))

#### Features

*   Anonymous tracking: You can send anonymous events, and we’ll reconcile anonymous activity with your users when you identify them.
*   Built-in lifecycle events: the SDK now automatically captures events like “Application Installed” and “Application Updated” for you, so you better understand your users’ behaviors in your app. ([#334](https://github.com/customerio/customerio-android/issues/334)) ([9d239c8](https://github.com/customerio/customerio-android/commit/9d239c895a975b7980c39e0ae0cb587cb1fb07a9))
*   New device data: the SDK captures complete device-level context, like your audience’s screen dimensions, device names, and more. ([#341](https://github.com/customerio/customerio-android/issues/341)) ([7cb17c1](https://github.com/customerio/customerio-android/commit/7cb17c1e8e7aa9f1ad8e71c01f2f80dab38ee7d9))

## 3.x Releases

* * *

### 3.11.2

August 19, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.11.2)

#### Bug Fixes

*   Resolves an issue that caused the SDK version to be reported as the previous version for versions 3.3.0 - 4.0.0. ([38b2815](https://github.com/customerio/customerio-android/commit/38b2815d1c1f36d7d34507e4c56dc271c8bb47bc))

### 3.11.1

July 15, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.11.1)

#### Bug Fixes

*   Messages created in the [new drag and drop in-app editor](https://customer.io/docs/release-notes/2024-07-15-in-app-visual-editor/) will load much faster and don’t require pre-fetching assets. If you are using the older in-app editor, the initial in-app message may experience a delay. We highly recommend upgrading to the new in-app editor to avoid this issue and ensure optimal performance. ([#393](https://github.com/customerio/customerio-android/issues/393)) ([8c8ee89](https://github.com/customerio/customerio-android/commit/8c8ee8900eeb0ef8008b8cb1b955b17fb0bd425e))
*   This update also resolves an issue that was causing crashes during the pre-fetching step in certain situations.

### 3.11.0

June 17, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.11.0)

#### Features

*   When using page rules and when an in-app messages need a second to load, the in-app message might appear after a user navigates to another page. We made changes to ensure the page-rules are checked after the message is loaded and immediately before it’s displayed in order to resolve this issue.
*   We resolved a caching issue that in rare cases, caused users to see the incorrect in-app message when logging out then logging back in as a different user.

([#355](https://github.com/customerio/customerio-android/issues/355)) ([ee756d6](https://github.com/customerio/customerio-android/commit/ee756d64a791657ec2f05cedb173a2870db923f1))

### 3.10.0

April 18, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.10.0)

#### Features

*   support for android gradle plugin 8 ([#296](https://github.com/customerio/customerio-android/issues/296)) ([90b31ef](https://github.com/customerio/customerio-android/commit/90b31ef28f1c276f90526631e2784579cf5a6702))

### 3.9.2

April 8, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.9.2)

#### Bug Fixes

*   added consumer rules for R8 full mode ([#298](https://github.com/customerio/customerio-android/issues/298)) ([3aa6757](https://github.com/customerio/customerio-android/commit/3aa675756266cf81de1f99adcb830079128c3584))

### 3.9.1

March 21, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.9.1)

#### Bug Fixes

*   last used format for device ([#287](https://github.com/customerio/customerio-android/issues/287)) ([726667b](https://github.com/customerio/customerio-android/commit/726667ba4380b21cdf88042fe1984596f08f82f1))

### 3.9.0

February 12, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.9.0)

#### Features

*   use new header to set polling interval ([#285](https://github.com/customerio/customerio-android/issues/285)) ([966c9cb](https://github.com/customerio/customerio-android/commit/966c9cbd03a896a48c120ae495847705382b82c9))

### 3.8.2

January 18, 2024 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.8.2)

#### Bug Fixes

*   ensure messages are only shown once ([#281](https://github.com/customerio/customerio-android/issues/281)) ([f73e13f](https://github.com/customerio/customerio-android/commit/f73e13fddedc6fe36a6b7afd12db94014fb89014))

### 3.8.1

December 6, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.8.1)

#### Bug Fixes

*   fixes a thread safety issue with currentMessage being out of sync ([#279](https://github.com/customerio/customerio-android/issues/279)) ([bb72944](https://github.com/customerio/customerio-android/commit/bb729449ad28e6b8afc1159d84c6ab63f732e526))

### 3.8.0

November 14, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.8.0)

#### Features

*   improve push click behavior ([#247](https://github.com/customerio/customerio-android/issues/247)) ([bcc5318](https://github.com/customerio/customerio-android/commit/bcc5318b1ea055cfaa29d5ee35ef6ae53e340f1b))

### 3.7.1

November 9, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.7.1)

#### Bug Fixes

*   add priority field to message and sorting by priority within local store ([#276](https://github.com/customerio/customerio-android/issues/276)) ([ddb0359](https://github.com/customerio/customerio-android/commit/ddb035983a9db5b5b7a711843a3dc15a8dc45a02))

### 3.7.0

October 25, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.7.0)

#### Features

*   persistant in-app messages ([#269](https://github.com/customerio/customerio-android/issues/269)) ([4fcb781](https://github.com/customerio/customerio-android/commit/4fcb781a3cf1a0e3b1414a6096deac40495250eb))

### 3.6.7

October 17, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.6.7)

#### Bug Fixes

*   added exception handling for when browser doesn't exist ([#271](https://github.com/customerio/customerio-android/issues/271)) ([aaddad5](https://github.com/customerio/customerio-android/commit/aaddad53281ac22c1e8a73340fcb24e7522e4648))

### 3.6.6

September 15, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.6.6)

#### Bug Fixes

*   stack-overflow caused by BQ recursion ([#251](https://github.com/customerio/customerio-android/issues/251)) ([365a5b6](https://github.com/customerio/customerio-android/commit/365a5b690ed37667dfb6782629ad56743d97904d))

### 3.6.5

August 23, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.6.5)

#### Bug Fixes

*   prevent concurrency issues in in-app listeners ([#246](https://github.com/customerio/customerio-android/issues/246)) ([72dafd7](https://github.com/customerio/customerio-android/commit/72dafd7e55091dbf64719fadc1cc9ff4010a00a4))

### 3.6.4

July 21, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.6.4)

#### Bug Fixes

*   in-app messaging proguard rules missing ([#241](https://github.com/customerio/customerio-android/issues/241)) ([c494bb0](https://github.com/customerio/customerio-android/commit/c494bb01ed4c3a7e552e381a88b3e27264c553ba))

### 3.6.3

July 14, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.6.3)

#### Bug Fixes

*   prevent empty identifier and device token ([#240](https://github.com/customerio/customerio-android/issues/240)) ([e9b5d0c](https://github.com/customerio/customerio-android/commit/e9b5d0cf74e52dde042ffc6a6412f443821d24bf))

### 3.6.2

July 13, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.6.2)

#### Bug Fixes

*   duplicate classes crash on wrappers ([#239](https://github.com/customerio/customerio-android/issues/239)) ([ead2175](https://github.com/customerio/customerio-android/commit/ead217559c675b561410ea97886b4d4dfa6af2c0))

### 3.6.1

July 12, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.6.1)

#### Bug Fixes

*   migrate in-app module from gist to CIO ([#221](https://github.com/customerio/customerio-android/issues/221)) ([d6fda6d](https://github.com/customerio/customerio-android/commit/d6fda6df4c81b2499039cae857a7bbd3ccccafd6))

> Note: We've made updates to our [installation instructions](https://www.customer.io/docs/sdk/android/getting-started/#install). Please refer to them as they reflect our new streamlined process which no longer necessitates a previously required dependency for in-app messages.

### 3.6.0

July 7, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.6.0)

#### Features

*   expose device token ([#235](https://github.com/customerio/customerio-android/issues/235)) ([deaa916](https://github.com/customerio/customerio-android/commit/deaa9164d7a9666823dfa4e6c09e3c20d6a3cfeb))

### 3.5.3

July 3, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.5.3)

#### Bug Fixes

*   expose push tracking to wrapper sdks ([#227](https://github.com/customerio/customerio-android/issues/227)) ([3bc1345](https://github.com/customerio/customerio-android/commit/3bc134507f1539329a4ed015e7b0ae5a76d109e0))

### 3.5.2

June 9, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.5.2)

#### Bug Fixes

*   set auto track screen to false by default ([#216](https://github.com/customerio/customerio-android/issues/216)) ([71fcf52](https://github.com/customerio/customerio-android/commit/71fcf528bb52f7d2fcc6c9cecf686927e3c9b77b))

### 3.5.1

June 1, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.5.1)

#### Bug Fixes

*   improve delivered metrics ([#204](https://github.com/customerio/customerio-android/issues/204)) ([831d1d9](https://github.com/customerio/customerio-android/commit/831d1d92c7abcf861b73e1301fbed57b0945b44b))

### 3.5.0

May 26, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.5.0)

#### Features

*   dismiss in-app message ([#186](https://github.com/customerio/customerio-android/issues/186)) ([89794f3](https://github.com/customerio/customerio-android/commit/89794f32ed7d8de3f699bfa5f985c179c5b3762c))

### 3.4.3

May 19, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.4.3)

#### Bug Fixes

*   minor only auto update for gist ([#210](https://github.com/customerio/customerio-android/issues/210)) ([c00d50c](https://github.com/customerio/customerio-android/commit/c00d50c22e8c0e21d445d678833fc0bb81b70d0b))

### 3.4.2

April 22, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.4.2)

#### Bug Fixes

*   multiple Queue instances being created ([#190](https://github.com/customerio/customerio-android/issues/190)) ([406626c](https://github.com/customerio/customerio-android/commit/406626c467bb65616fd66184bc0fc5005b3b335b))

### 3.4.1

April 20, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.4.1)

#### Bug Fixes

*   push opened metrics tracked on Android 12 ([#184](https://github.com/customerio/customerio-android/issues/184)) ([d2e52fa](https://github.com/customerio/customerio-android/commit/d2e52fa35be34cf0248d3c775614f487234cf8d4))

### 3.4.0

April 19, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.4.0)

#### Features

*   in app click tracking ([#187](https://github.com/customerio/customerio-android/issues/187)) ([4ad1f35](https://github.com/customerio/customerio-android/commit/4ad1f35c6ba5e67f07dd78f1169ef4b9b1ed547a))

### 3.3.2

March 10, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.3.2)

#### Bug Fixes

*   remove tasks from queue with 400 http response ([#177](https://github.com/customerio/customerio-android/issues/177)) ([3ed104a](https://github.com/customerio/customerio-android/commit/3ed104a65be9cb673e4ab694335adef5d0047b8d))

### 3.3.1

March 7, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.3.1)

#### Bug Fixes

*   prevent crash for file not found exception ([#178](https://github.com/customerio/customerio-android/issues/178)) ([be8a2d9](https://github.com/customerio/customerio-android/commit/be8a2d9776114a2b8b86c6972c29dc6aec5e02f1))

### 3.3.0

February 22, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.3.0)

#### Features

*   add setting a in-app event listener ([#147](https://github.com/customerio/customerio-android/issues/147)) ([5fd9559](https://github.com/customerio/customerio-android/commit/5fd95590788b518c3fddcea60795c04e128c49a7))
*   in-app feature no longer requires orgId ([#163](https://github.com/customerio/customerio-android/issues/163)) ([fc2a08e](https://github.com/customerio/customerio-android/commit/fc2a08eda19a2f6387790aec442512fb115e0ea0))

#### Bug Fixes

*   image not shown when image url in notification payload ([#172](https://github.com/customerio/customerio-android/issues/172)) ([0abdc85](https://github.com/customerio/customerio-android/commit/0abdc85ba5213f758e03a378d9374b89dd28d335))
*   moved shared wrapper code ([#158](https://github.com/customerio/customerio-android/issues/158)) ([51af98f](https://github.com/customerio/customerio-android/commit/51af98f13ec1ab26fbd2bbc160a817eb19ccb080))
*   remove currentRoute parameter in in-app event listener ([#159](https://github.com/customerio/customerio-android/issues/159)) ([688e4a5](https://github.com/customerio/customerio-android/commit/688e4a53a031b02a1ef81b61e328b1a35cd77381))
*   rename in app listener keys ([#164](https://github.com/customerio/customerio-android/issues/164)) ([f540eaf](https://github.com/customerio/customerio-android/commit/f540eaf10310ab97e57912fe5382f39319a565af))
*   set gist dependency to use latest 3.X.Y version ([#170](https://github.com/customerio/customerio-android/issues/170)) ([a019c36](https://github.com/customerio/customerio-android/commit/a019c36cc1fd37e6180170d2d5f703e2ac8c48e8))
*   set gist user token incase identifier exists ([#162](https://github.com/customerio/customerio-android/issues/162)) ([44cc4d1](https://github.com/customerio/customerio-android/commit/44cc4d11fc11a2e96fcdc0fa1b83f012fb25fbd5))
*   update CustomerIOFirebaseMessagingService to open ([#174](https://github.com/customerio/customerio-android/issues/174)) ([edce7f5](https://github.com/customerio/customerio-android/commit/edce7f5b640a76b1fdfa029b9b0dabea281c677c))
*   upgrade dependencies ([#146](https://github.com/customerio/customerio-android/issues/146)) ([6da8b8d](https://github.com/customerio/customerio-android/commit/6da8b8d3d16fa1c9c0acdb6012271a2252b30951))
*   use maven style dependency range syntax ([#171](https://github.com/customerio/customerio-android/issues/171)) ([ba83214](https://github.com/customerio/customerio-android/commit/ba83214ec1218b36bc824ddca355a6fd5041b65e))

### 3.2.0

February 2, 2023 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.2.0)

#### Features

*   sdk initialization re-architecture ([9e21960](https://github.com/customerio/customerio-android/commit/9e219600c45554425841d55c8ccc97891514141f))

#### Bug Fixes

*   cio sdk version attribute using client value ([bb90f35](https://github.com/customerio/customerio-android/commit/bb90f35061a44c1adc2ed68ed90dfa7e1860f2a4))
*   user agent client support in preferences repo ([454a18e](https://github.com/customerio/customerio-android/commit/454a18eec3c4716cb809db6c91d505fa21ba5350))

### 3.1.1

November 28, 2022 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.1.1)

#### Bug Fixes

*   in-app messages instant delivery ([#150](https://github.com/customerio/customerio-android/issues/150)) ([a6dcf3c](https://github.com/customerio/customerio-android/commit/a6dcf3c190d25cc7052aa3e749b6258beafed1f7))

### 3.1.0

October 17, 2022 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.1.0)

#### Features

*   added shared instance for independent components ([70fa8cd](https://github.com/customerio/customerio-android/commit/70fa8cd69079c94cd20d5e9bc02e563796c5e52a))
*   added support to modify notification small icon ([b93c2dc](https://github.com/customerio/customerio-android/commit/b93c2dc653c32d58da83e4a01afffef01b0fbfd1))

#### Bug Fixes

*   in-app messages stop delivering ([4027502](https://github.com/customerio/customerio-android/commit/40275020b73748864fab504234057170bd5b5561))
*   in-app system link causes app reopen ([6349081](https://github.com/customerio/customerio-android/commit/6349081c74bf595b06dd1157a382c6c4b884ac55))
*   updated json adapter usage to safe parsing ([f72280b](https://github.com/customerio/customerio-android/commit/f72280b3435be18274d81aee71303ae6ca9fda01))

### 3.0.0

October 5, 2022 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/3.0.0)

#### ⚠ BREAKING CHANGES

*   android 12 deep link fix

#### Features

*   added option to customize push notification from app ([68010f8](https://github.com/customerio/customerio-android/commit/68010f84e39872dc5b0d8cfffda8f169efeaa472))
*   changes for react native package ([2f20ac3](https://github.com/customerio/customerio-android/commit/2f20ac3dd1c1ba6be215d5206b22c143e37efe94))
*   in app sdk ([1036c80](https://github.com/customerio/customerio-android/commit/1036c8030259eaef1472e8c004636aee02d1af8a))
*   updated client to support react native user agent ([7588526](https://github.com/customerio/customerio-android/commit/7588526bef0e7bfc130b1b5a2cc8fd915bff3483))

#### Bug Fixes

*   android 12 deep link fix ([fd7ae28](https://github.com/customerio/customerio-android/commit/fd7ae288a9c85d8ba397419e1d20f58883f83020))
*   version bump for gist sdk to resolve messaging not showing bug ([05dad42](https://github.com/customerio/customerio-android/commit/05dad421fc938431e459daadd6a83b6cc3b9d33e))

## 2.x Releases

* * *

### 2.1.1

August 1, 2022 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/2.1.1)

#### Bug Fixes

*   parsing exception for expired tasks ([440bb13](https://github.com/customerio/customerio-android/commit/440bb134a4a234f2bc15354dc0ad6ca90a1b5da8))

### 2.1.0

August 1, 2022 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/2.1.0)

#### Features

*   delete expired background queue tasks ([8dca8b7](https://github.com/customerio/customerio-android/commit/8dca8b719f634d06c86628f154b2ff45d1bd6c79))

#### Bug Fixes

*   deploy code script commands ([#124](https://github.com/customerio/customerio-android/issues/124)) ([fe817d1](https://github.com/customerio/customerio-android/commit/fe817d10ee00ec5694d3b3e2caa8658c7cc9a1b1))

### 2.0.1

June 13, 2022 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/2.0.1)

#### Bug Fixes

*   track opens fcm notification payload ([ab3cd18](https://github.com/customerio/customerio-android/commit/ab3cd18416463935ad6c09c869bc1e895d08a4b2))

### 2.0.0

June 1, 2022 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/2.0.0)

#### ⚠ BREAKING CHANGES

*   get current FCM token on SDK startup
*   register device attributes when set
*   create background queue to make API synchronous

#### Features

*   add device and profile attribute setters ([789f09f](https://github.com/customerio/customerio-android/commit/789f09f0c58e21cb3200ad933a269c40e9f530dc))
*   add device\_manufacturer device attribute ([6a60f39](https://github.com/customerio/customerio-android/commit/6a60f3900c08e9e2bccc2fc76986dc4b66123c95))
*   added support for custom track url ([b61a64b](https://github.com/customerio/customerio-android/commit/b61a64b1f17c6b7d98ec1a743ffd07bef6819a4f))
*   added support for device attributes ([#71](https://github.com/customerio/customerio-android/issues/71)) ([5fedf26](https://github.com/customerio/customerio-android/commit/5fedf26f251259601ab89c15b5cd691361c30e9d))
*   create background queue to make API synchronous ([2524460](https://github.com/customerio/customerio-android/commit/2524460e84531150b61647acc0d5156cd9a4b3b9))
*   get current FCM token on SDK startup ([dda443d](https://github.com/customerio/customerio-android/commit/dda443d5f5d1f92edafe7e2f19a3e948d4d5d8f5))
*   register device attributes when set ([0f5159e](https://github.com/customerio/customerio-android/commit/0f5159ef5848b6bcd7641351bae90e2938ee5677))
*   set log level via SDK config ([81eea4e](https://github.com/customerio/customerio-android/commit/81eea4e00c518499f0de8052ec31aa87dd0ee31c))
*   support for custom device attributes and config ([#77](https://github.com/customerio/customerio-android/issues/77)) ([a7dbaba](https://github.com/customerio/customerio-android/commit/a7dbaba4d82e5e7378f590425ee8b911072036a8))

#### Bug Fixes

*   added java compatibility in public constructors and methods ([acdec46](https://github.com/customerio/customerio-android/commit/acdec46cece2e3dc44186bc7ff3c690bf48830dc))
*   added support for big decimal ([#56](https://github.com/customerio/customerio-android/issues/56)) ([58c791b](https://github.com/customerio/customerio-android/commit/58c791b8a6c9df91506cf75064622a399ca9a8d0))
*   code cleanup ([c651ee6](https://github.com/customerio/customerio-android/commit/c651ee62e1a7698e4efad0376aab5710655a2e2f))
*   delete device token when clear identify ([72f9753](https://github.com/customerio/customerio-android/commit/72f9753a721ea9de80cb63edf036f1daaf66f9ca))
*   events are tracked to identified customer ([71634ed](https://github.com/customerio/customerio-android/commit/71634edf4c36538d459f8140c0c05a426b4f21b2))
*   queue attempts to run all tasks on each run ([e180dea](https://github.com/customerio/customerio-android/commit/e180dea2e4fd8c334b0fe24185c179f42d6e8027))
*   register device tokens http request ([b1c6872](https://github.com/customerio/customerio-android/commit/b1c68724ccb5218bfcd3e8b25743e309ddf83b26))

## 1.x Releases

* * *

### 1.0.5

April 30, 2022 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/1.0.5)

#### Bug Fixes

*   added alternative route to fetch screen name for automatic tracking ([#92](https://github.com/customerio/customerio-android/issues/92)) ([37a20b5](https://github.com/customerio/customerio-android/commit/37a20b59da8778e9eba3baf067c9276ca878806d))

### 1.0.4

March 21, 2022 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/1.0.4)

#### Bug Fixes

*   crash using invalid characters in HTTP header ([#75](https://github.com/customerio/customerio-android/issues/75)) ([81a065e](https://github.com/customerio/customerio-android/commit/81a065ee44e1df58b3009287966e913f2888a6e1))

### 1.0.3

March 15, 2022 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/1.0.3)

#### Bug Fixes

*   allow http requests in host app ([#74](https://github.com/customerio/customerio-android/issues/74)) ([1035648](https://github.com/customerio/customerio-android/commit/103564882df611e956a54cba0c2635acd3f1997a))

### 1.0.2

March 11, 2022 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/1.0.2)

#### Bug Fixes

*   added java compatibility in public constructors and methods ([2bb73be](https://github.com/customerio/customerio-android/commit/2bb73be50dd66ef7e308d587b19daaf66c7e0968))

### 1.0.1

January 25, 2022 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/1.0.1)

#### Bug Fixes

*   number parsing coverts to double ([#57](https://github.com/customerio/customerio-android/issues/57)) ([72a80fe](https://github.com/customerio/customerio-android/commit/72a80fe38932a199dda68b3fcbfd10b3d025f450))

### 1.0.0

January 19, 2022 · [View on GitHub](https://github.com/customerio/customerio-android/releases/tag/1.0.0)

#### Bug Fixes

*   pro-guarding removal of enums ([#39](https://github.com/customerio/customerio-android/issues/39)) ([dd27d35](https://github.com/customerio/customerio-android/commit/dd27d3567172b5c8b1e0bdd989126afa8f290541))
*   timestamp conversion issue ([#37](https://github.com/customerio/customerio-android/issues/37)) ([d986f54](https://github.com/customerio/customerio-android/commit/d986f546c9200fd5477de719db2e6b804fe9308f))
*   updated host endpoints ([#53](https://github.com/customerio/customerio-android/issues/53)) ([efe2b9e](https://github.com/customerio/customerio-android/commit/efe2b9e36e0fe86abd9666f2d3af4fb84eadcdfb))
*   user agent ([#42](https://github.com/customerio/customerio-android/issues/42)) ([37af83e](https://github.com/customerio/customerio-android/commit/37af83ed0f73d4123354f2cf8d68befbd172c8f8))

#### Features

*   automatic screen tracking ([#52](https://github.com/customerio/customerio-android/issues/52)) ([8f95ebb](https://github.com/customerio/customerio-android/commit/8f95ebb59a1dbb94060a44a3db21b8bd2bac5723))
*   event tracking, identify customer ([#29](https://github.com/customerio/customerio-android/issues/29)) ([5181268](https://github.com/customerio/customerio-android/commit/51812682a79154b9fdc38ac604fb0cc2c4c74156))
*   manual screen tracking ([#51](https://github.com/customerio/customerio-android/issues/51)) ([213a278](https://github.com/customerio/customerio-android/commit/213a278e668f0e26ffef5983d6357b5370f96ad2))
*   Push notification ([#31](https://github.com/customerio/customerio-android/issues/31)) ([c639802](https://github.com/customerio/customerio-android/commit/c639802a44ea442212c65155efeb470f7c6ac64e))
*   Rich push support ([#34](https://github.com/customerio/customerio-android/issues/34)) ([2480127](https://github.com/customerio/customerio-android/commit/2480127f976f01f91c3d75f55ffc2589a2b58f59))

---

