Using the Android App Messaging SDK

Our Android App Messaging SDK uses Firebase Cloud Messaging (FCM) to send push notifications to your Android app users. If you haven't already configured a Firebase project then please follow the instructions here first.

Our Android SDK is open source and can be found on Github here:

Basic sample apps can be found in Github here:

To embed in your native Android apps, complete the following tasks:

  1. Install the SDK
  2. Prepare the Push and Challenge handler classes
  3. Initialise the SDK
  4. Handle push message callbacks
  5. Optional: Change the icon or colour of your push notifications
  6. Starting a session
📘

Handling asynchronous requests

You can use either standard callbacks or observable streams to handle asynchronous requests. If you want to use observable streams, the SDK uses RxJava 1 (rx.*), not RxJava 2/3 — make sure the imports match. This tutorial uses standard callbacks, with Kotlin coroutine equivalents shown where helpful.

Installing the SDK

  1. Add the App Messaging SDK to your module-level build.gradle (Groovy DSL) or build.gradle.kts (Kotlin DSL). The SDK requires Java 11, so set sourceCompatibility and targetCompatibility to JavaVersion.VERSION_11, and add the com.comapi:foundation dependency.
android {
    compileSdk 36

    defaultConfig {
        // ...
        minSdkVersion 21
        targetSdkVersion 36
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }
}

dependencies {
    // ...
    implementation 'com.comapi:foundation:1.6.0'

    // Firebase Cloud Messaging — the SDK exposes RemoteMessage in its public API
    implementation platform('com.google.firebase:firebase-bom:33.1.0')
    implementation 'com.google.firebase:firebase-messaging'
}
android {
    compileSdk = 36

    defaultConfig {
        // ...
        minSdk = 21
        targetSdk = 36
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
}

dependencies {
    // ...
    implementation("com.comapi:foundation:1.6.0")

    // Firebase Cloud Messaging — the SDK exposes RemoteMessage in its public API
    implementation(platform("com.google.firebase:firebase-bom:33.1.0"))
    implementation("com.google.firebase:firebase-messaging")
}
📘

About the Firebase BoM. The Firebase Android BoM (Bill of Materials) keeps all Firebase library versions aligned automatically — declare one BoM version and leave the individual Firebase artifacts unversioned. Check the Firebase Android release notes for the current BoM version.

🚧

Seeing a "Manifest merger" error?

The most common cause is minSdkVersion in your app being lower than the SDK supports. Setting it to 21 (or higher) usually resolves it. The SDK is built against Java 11, so your module must also be on Java 11.

  1. Add the Google Services plugin to your project-level (top-level) build file using the Plugin DSL. Declaring it here with apply false lets you apply it in any module that needs it.
plugins {
    id 'com.google.gms.google-services' version '4.4.2' apply false
}
plugins {
    id("com.google.gms.google-services") version "4.4.2" apply false
}
  1. Apply the plugin in your module-level build file. This is the step that wires google-services.json into the build — without it, the file isn't processed.
plugins {
    id 'com.android.application'
    id 'com.google.gms.google-services'
}
plugins {
    id("com.android.application")
    id("com.google.gms.google-services")
}
  1. Copy the google-services.json file from your Firebase project into your app module folder (typically app/google-services.json) — not the project root.
👍

Sync Now

Synchronise your Gradle files (Android Studio: File → Sync Project with Gradle Files) after making any changes.

Prepare the Push and Challenge handler classes

Before you can configure the Android SDK, you need:

  1. The value of the API space ID field in Dotdigital.
  2. A class that creates a JWT token.
  3. A placeholder push handler class implementing the PushMessageListener interface. The PushHandler is the class you pass into the SDK init method. Inside it you'll receive push messages delivered while the app is visible to the user.
import com.comapi.internal.push.PushMessageListener;
import com.google.firebase.messaging.RemoteMessage;

public class PushHandler implements PushMessageListener {
    @Override
    public void onMessageReceived(RemoteMessage message) {
        // TODO: Add push message handling such as displaying messages when in foreground
        // and handling deep links and custom data.
    }
}
import com.comapi.internal.push.PushMessageListener
import com.google.firebase.messaging.RemoteMessage

class PushHandler : PushMessageListener {
    override fun onMessageReceived(message: RemoteMessage) {
        // TODO: Add push message handling such as displaying messages when in foreground
        // and handling deep links and custom data.
    }
}

Initialising the Android SDK

🚧

Where to initialise the Android SDK

Initialisation should be performed in the onCreate() of your Application subclass so the user's profile doesn't change every time they open the app. Remember to register that class in the manifest: <application android:name=".MyApplication">.

📘

Firebase is initialised automatically

The Firebase Android SDK ships a ContentProvider (com.google.firebase.provider.FirebaseInitProvider) that runs before your Application.onCreate(), so FirebaseApp is guaranteed to be ready by the time the Comapi SDK initialises. You do not need to call FirebaseApp.initializeApp(context) yourself unless you're using a non-default FirebaseOptions configuration.

  1. Create a new instance of the ComapiConfig class, pass your API Space ID to apiSpaceId(...), your authentication handler to authenticator(...), and your push handler to pushMessageListener(...):
import com.comapi.ComapiConfig;

ComapiConfig config = new ComapiConfig()
    // Set the id of the API space this device belongs to
    .apiSpaceId("<API_SPACE_ID>")
    // Handler for authentication challenges (SDK asking for a JWT)
    .authenticator(new ChallengeHandler())
    // Listener for pushes delivered while the app is in the foreground
    // (Android does not render system-tray notifications in this case).
    .pushMessageListener(new PushHandler());
import com.comapi.ComapiConfig

val config = ComapiConfig()
    .apiSpaceId("<API_SPACE_ID>")
    .authenticator(ChallengeHandler())
    .pushMessageListener(PushHandler())

Configuring logs and proxy servers

LogConfig lets you set the level independently for three sinks: file (the SDK's internal rolling log), console (Logcat), and network (HTTP request/response logging). The defaults are all WARNING. Available levels: OFF, FATAL, ERROR, WARNING, INFO, DEBUG.

import com.comapi.internal.log.LogConfig;
import com.comapi.internal.log.LogLevel;

LogLevel level = BuildConfig.DEBUG ? LogLevel.DEBUG : LogLevel.WARNING;

config.logConfig(
    new LogConfig()
        .setFileLevel(level)
        .setConsoleLevel(level)
        .setNetworkLevel(level)
);
import com.comapi.internal.log.LogConfig
import com.comapi.internal.log.LogLevel

val level = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARNING

config.logConfig(
    LogConfig()
        .setFileLevel(level)
        .setConsoleLevel(level)
        .setNetworkLevel(level)
)
🔒

Privacy note: DEBUG and INFO network logs typically include request URLs, headers, and bodies. Never ship a release build at anything above WARNING.

Set a custom limit for the internal log file size:

config.logSizeLimitKilobytes(2048);
config.logSizeLimitKilobytes(2_048)

If your app connects through a proxy server (for example, when debugging through Charles or mitmproxy):

import com.comapi.APIConfig;

config.apiConfiguration(new APIConfig().proxy("http://10.0.2.2:8888")); // emulator → host
import com.comapi.APIConfig

config.apiConfiguration(APIConfig().proxy("http://10.0.2.2:8888")) // emulator → host

  1. Pass the ComapiConfig to one of the initialisation methods:

    • Comapi.initialiseShared(...) — the SDK stores the client as a singleton. Access it later via Comapi.getShared().
    • Comapi.initialise(...) — non-singleton variant; you store the client yourself (e.g. in your DI container).
📦

Comapi.getShared() after init. Comapi.getShared() and RxComapi.getShared() return the non-null singleton once initialiseShared(...) has been called — the client is created synchronously inside initialiseShared(...), before the async network init completes. They only throw RuntimeException if you call them before initialiseShared(...) has run at all. With init in Application.onCreate(), getShared() is safe to call from any Activity, Service, or BroadcastReceiver.

package com.example.testapp;

import android.app.Application;
import com.comapi.Callback;
import com.comapi.Comapi;
import com.comapi.ComapiClient;
import com.comapi.ComapiConfig;

public class MyApplication extends Application implements Callback<ComapiClient> {

    @Override
    public void onCreate() {
        super.onCreate();

        ComapiConfig config = new ComapiConfig()
            .apiSpaceId("<API_SPACE_ID>")
            .authenticator(new ChallengeHandler())
            .pushMessageListener(new PushHandler(this));

        // Asynchronously initialise the SDK. The Application class implements Callback<ComapiClient>,
        // so the success / error methods below are invoked when init finishes.
        // initialiseShared makes the client retrievable later via Comapi.getShared().
        Comapi.initialiseShared(this, config, this);
    }

    @Override
    public void success(ComapiClient client) {
        // SDK initialised — client is also available via Comapi.getShared()
    }

    @Override
    public void error(Throwable t) {
        // SDK init failed
    }
}
package com.example.testapp

import android.app.Application
import com.comapi.Callback
import com.comapi.Comapi
import com.comapi.ComapiClient
import com.comapi.ComapiConfig

class MyApplication : Application(), Callback<ComapiClient> {

    override fun onCreate() {
        super.onCreate()

        val config = ComapiConfig()
            .apiSpaceId("<API_SPACE_ID>")
            .authenticator(ChallengeHandler())
            .pushMessageListener(PushHandler(this))

        // Asynchronously initialise the SDK. The Application class implements Callback<ComapiClient>,
        // so the success / error methods below are invoked when init finishes.
        Comapi.initialiseShared(this, config, this)
    }

    override fun success(client: ComapiClient) {
        // SDK initialised — client is also available via Comapi.getShared()
    }

    override fun error(t: Throwable) {
        // SDK init failed
    }
}

Register your Application subclass in AndroidManifest.xml:

<application
    android:name=".MyApplication"
    ... >
    ...
</application>
  1. Request the runtime notification permission on Android 13+ (API 33). Use the modern Activity Result API rather than requestPermissions(...) directly — it's the recommended path and survives configuration changes cleanly.
public class MainActivity extends AppCompatActivity {

    private final ActivityResultLauncher<String> requestNotificationPermission =
            registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> {
                // granted == true means the user accepted the prompt
            });

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ensureNotificationPermission();
    }

    private void ensureNotificationPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
                && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
                    != PackageManager.PERMISSION_GRANTED) {
            requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS);
        }
    }
}
class MainActivity : AppCompatActivity() {

    private val requestNotificationPermission =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) {
            // granted == true means the user accepted the prompt
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ensureNotificationPermission()
    }

    private fun ensureNotificationPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
            ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
            != PackageManager.PERMISSION_GRANTED
        ) {
            requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
        }
    }
}

The SDK's manifest already declares POST_NOTIFICATIONS. Without the user granting it at runtime on Android 13+, the OS silently drops every notification — including the ones Android renders automatically while your app is backgrounded.


Handle push message callbacks

Flesh out the push handler class — onMessageReceived is called whenever the SDK receives a push message while the app is in the foreground.

Push handler callback

Within your push handler class implementing PushMessageListener, decide what you want to do with the push and implement it inside onMessageReceived().

Displaying push notifications when the app is in the foreground

Android only renders push notifications automatically when the app is in the background — these notifications appear in the system tray and launch your app when tapped. To display a push while your app is in the foreground you need to do it yourself.

The example below builds a NotificationCompat.Builder notification, sets a back-stacked intent so tapping it routes through your launcher Activity, and guards the notify() call on the POST_NOTIFICATIONS permission for Android 13+.

package com.example.testapp;

import android.Manifest;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;

import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.TaskStackBuilder;

import com.comapi.ComapiClient;
import com.comapi.internal.push.PushMessageListener;
import com.google.firebase.messaging.RemoteMessage;

import org.json.JSONException;

public class PushHandler implements PushMessageListener {

    private static final String CHANNEL_ID = "comapi_default_channel";
    private static final int NOTIFICATION_ID = 1;

    private final Context context;

    public PushHandler(Context context) {
        this.context = context.getApplicationContext();
    }

    @Override
    public void onMessageReceived(RemoteMessage message) {
        // Let the SDK parse the dotdigital deep-link URL / custom data (if present).
        try {
            ComapiClient.parsePushMessage(message);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        RemoteMessage.Notification notification = message.getNotification();
        if (notification == null) return; // data-only payload

        createNotificationChannel();

        // Intent that re-launches MainActivity with the original push intent extras.
        Intent resultIntent = new Intent(context, MainActivity.class);
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
        stackBuilder.addNextIntentWithParentStack(resultIntent);
        PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(
                0, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
                .setAutoCancel(true)
                .setDefaults(NotificationCompat.DEFAULT_ALL)
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.drawable.ic_notification)
                .setContentTitle(notification.getTitle())
                .setContentText(notification.getBody())
                .setContentIntent(resultPendingIntent);

        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
                == PackageManager.PERMISSION_GRANTED) {
            NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, builder.build());
        }
    }

    private void createNotificationChannel() {
        // Notification channels were introduced in API 26.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(
                    CHANNEL_ID,
                    "Default",
                    NotificationManager.IMPORTANCE_DEFAULT);
            channel.setDescription("Default push notifications");

            NotificationManager nm = context.getSystemService(NotificationManager.class);
            if (nm != null) {
                nm.createNotificationChannel(channel);
            }
        }
    }
}
package com.example.testapp

import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import com.comapi.ComapiClient
import com.comapi.internal.push.PushMessageListener
import com.google.firebase.messaging.RemoteMessage
import org.json.JSONException

class PushHandler(context: Context) : PushMessageListener {

    private val appContext = context.applicationContext

    override fun onMessageReceived(message: RemoteMessage) {
        try {
            ComapiClient.parsePushMessage(message)
        } catch (e: JSONException) {
            e.printStackTrace()
        }

        val notification = message.notification ?: return // data-only payload

        createNotificationChannel()

        val resultIntent = Intent(appContext, MainActivity::class.java)
        val stackBuilder = TaskStackBuilder.create(appContext)
        stackBuilder.addNextIntentWithParentStack(resultIntent)
        val resultPendingIntent: PendingIntent? = stackBuilder.getPendingIntent(
            0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )

        val builder = NotificationCompat.Builder(appContext, CHANNEL_ID)
            .setAutoCancel(true)
            .setDefaults(NotificationCompat.DEFAULT_ALL)
            .setWhen(System.currentTimeMillis())
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(notification.title)
            .setContentText(notification.body)
            .setContentIntent(resultPendingIntent)

        if (ActivityCompat.checkSelfPermission(appContext, Manifest.permission.POST_NOTIFICATIONS)
            == PackageManager.PERMISSION_GRANTED
        ) {
            NotificationManagerCompat.from(appContext).notify(NOTIFICATION_ID, builder.build())
        }
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                "Default",
                NotificationManager.IMPORTANCE_DEFAULT
            ).apply { description = "Default push notifications" }

            appContext.getSystemService(NotificationManager::class.java)
                ?.createNotificationChannel(channel)
        }
    }

    companion object {
        private const val CHANNEL_ID = "comapi_default_channel"
        private const val NOTIFICATION_ID = 1
    }
}

Handling deep links

The SDK can handle deep links for you and track their usage — this is the recommended setup.

To enable deep-link handling:

  1. Declare an intent filter on the Activity that should open when the deep link is invoked. For a deep link of the form myappscheme://mycustomhost:
<activity
    android:name=".MyActivity"
    android:exported="true">
    <intent-filter android:label="Open Activity in DotDigital Sample App">
        <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 "myappscheme://mycustomhost" -->
        <data
            android:host="mycustomhost"
            android:scheme="myappscheme" />
    </intent-filter>
</activity>
  1. In your launcher Activity, route the Intent from onCreate(...) and onNewIntent(...) into client.handlePushNotification(...). The SDK reads the dotdigital-specific extras the system places on the launcher intent when a system-tray notification is tapped, records the click for analytics, and (optionally) launches the deep-link target.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        handlePush(getIntent());
    }

    // If you use android:launchMode="singleTop", a new intent is delivered here
    // instead of recreating the Activity.
    @Override
    protected void onNewIntent(@NonNull Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
        handlePush(intent);
    }

    private void handlePush(@NonNull Intent intent) {
        // Safe to call once Application.onCreate has run Comapi.initialiseShared(...).
        ComapiClient client = Comapi.getShared();
        client.handlePushNotification(this, intent, /* startActivity = */ true,
            new Callback<PushHandleResult>() {
                @Override public void success(PushHandleResult result) {
                    if (result == null) return;
                    String url = result.getUrl();                 // dotdigital deep link
                    JSONObject data = result.getData();           // custom data payload
                    boolean clickRecorded = result.isClickRecorded();
                    boolean deepLinkLaunched = result.isDeepLinkCalled();
                    // Route within your app as appropriate.
                }
                @Override public void error(Throwable t) { /* ... */ }
            });
    }
}
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        handlePush(intent)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        handlePush(intent)
    }

    private fun handlePush(intent: Intent) {
        // Safe to call once Application.onCreate has run Comapi.initialiseShared(...).
        val client = Comapi.getShared()
        client.handlePushNotification(this, intent, /* startActivity = */ true,
            object : Callback<PushHandleResult> {
                override fun success(result: PushHandleResult?) {
                    result ?: return
                    val url: String? = result.url                  // dotdigital deep link
                    val data: JSONObject? = result.data            // custom data payload
                    val clickRecorded: Boolean = result.isClickRecorded
                    val deepLinkLaunched: Boolean = result.isDeepLinkCalled
                    // Route within your app as appropriate.
                }

                override fun error(t: Throwable) { /* ... */ }
            })
    }
}
📘

What the callback flags mean. PushHandleResult.isClickRecorded() indicates the click was tracked against dotdigital analytics. PushHandleResult.isDeepLinkCalled() indicates the SDK successfully launched the deep-link ACTION_VIEW intent. Either may be false if the payload didn't contain a deep link or if no Activity was registered for the URL's scheme/host.

Extracting a deep link without launching it

If you want to extract the deep-link URL yourself rather than have the SDK launch it, use the static ComapiClient.parsePushMessage(...) helper. It throws JSONException if the payload is malformed.

public class PushHandler implements PushMessageListener {
    @Override
    public void onMessageReceived(RemoteMessage message) {
        try {
            PushDetails details = ComapiClient.parsePushMessage(message);
            Log.i("Comapi", "url = " + details.getUrl());
        } catch (JSONException e) {
            Log.w("Comapi", "Failed to parse push payload", e);
        }
    }
}
class PushHandler : PushMessageListener {
    override fun onMessageReceived(message: RemoteMessage) {
        try {
            val details = ComapiClient.parsePushMessage(message)
            Log.i("Comapi", "url = ${details.url}")
        } catch (e: JSONException) {
            Log.w("Comapi", "Failed to parse push payload", e)
        }
    }
}

Handling custom data payloads

parsePushMessage(...) also surfaces the dotdigital custom-data field. Read it with getData() on the returned PushDetails:

public class PushHandler implements PushMessageListener {
    @Override
    public void onMessageReceived(RemoteMessage message) {
        try {
            PushDetails details = ComapiClient.parsePushMessage(message);
            JSONObject data = details.getData();
            Log.i("Comapi", "data = " + (data != null ? data.toString() : "null"));
        } catch (JSONException e) {
            Log.w("Comapi", "Failed to parse push payload", e);
        }
    }
}
class PushHandler : PushMessageListener {
    override fun onMessageReceived(message: RemoteMessage) {
        try {
            val details = ComapiClient.parsePushMessage(message)
            Log.i("Comapi", "data = ${details.data?.toString() ?: "null"}")
        } catch (e: JSONException) {
            Log.w("Comapi", "Failed to parse push payload", e)
        }
    }
}

Changing the icon or colour of push notifications

The Android SDK uses Firebase Cloud Messaging (FCM) to deliver push notifications. Use the Android <meta-data> element in your AndroidManifest.xml to set the default icon and accent colour used when the server payload doesn't specify one.

<!-- Default icon for incoming notification messages, used when the payload doesn't supply one. -->
<meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@mipmap/ic_launcher_round" />

<!-- Default colour (accent) for incoming notification messages. -->
<meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/colorAccent" />

Sessions

Starting a session

To receive push messages and register push tokens the SDK requires an active session.

To create a session, you need a successfully initialised client and an identifier you can put into the sub claim of the JWT — typically the signed-in user's id, or a stable device-scoped GUID for anonymous users.

client.service().session().startSession(new Callback<Session>() {
    @Override public void success(Session session) {
        if (session.isSuccessfullyCreated()) {
            // Session is ready for profile " + session.getProfileId()
        }
    }
    @Override public void error(Throwable t) { /* ... */ }
});
client.service().session().startSession(object : Callback<Session> {
    override fun success(session: Session) {
        if (session.isSuccessfullyCreated) {
            // Session is ready for profile ${session.profileId}
        }
    }
    override fun error(t: Throwable) { /* ... */ }
})
rxClient.service().session().startSession()
    .subscribe(new Observer<Session>() { /* implement */ });
rxClient.service().session().startSession()
    .subscribe(object : Observer<Session> { /* implement */ })

Treat isSuccessfullyCreated() == false as a soft failure — the success path can still fire even when the server returned an incomplete session. Always check the flag before treating the SDK as authenticated.

Ending a session

End the current session only when the user signs out or when you want to swap users on the same device.

client.service().session().endSession(new Callback<ComapiResult<Void>>() { /* implement */ });
client.service().session().endSession(object : Callback<ComapiResult<Void>> { /* implement */ })
rxClient.service().session().endSession()
    .subscribe(new Observer<ComapiResult<Void>>() { /* implement */ });
rxClient.service().session().endSession()
    .subscribe(object : Observer<ComapiResult<Void>> { /* implement */ })

🚧

Only users that have both an email and push token will be created in Dotdigital

Please read the Registering your app users for push page to understand the requirements for syncing your app users with Dotdigital. Only users who have both an email address and a push token will be synced and eligible to receive push messages.

👍

Next steps

Now ensure your app passes an email address to the SDK for the app user so that a contact is created in Dotdigital by following these instructions.

📘

Want to know more about the SDK?

To find out more about the SDK and its features and functions please go here.