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:
- Install the SDK
- Prepare the Push and Challenge handler classes
- Initialise the SDK
- Handle push message callbacks
- Optional: Change the icon or colour of your push notifications
- Starting a session
Handling asynchronous requestsYou 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
- Add the App Messaging SDK to your module-level
build.gradle(Groovy DSL) orbuild.gradle.kts(Kotlin DSL). The SDK requires Java 11, so setsourceCompatibilityandtargetCompatibilitytoJavaVersion.VERSION_11, and add thecom.comapi:foundationdependency.
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
minSdkVersionin 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.
- Add the Google Services plugin to your project-level (top-level) build file using the Plugin DSL. Declaring it here with
apply falselets 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
}- Apply the plugin in your module-level build file. This is the step that wires
google-services.jsoninto 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")
}- Copy the
google-services.jsonfile from your Firebase project into your app module folder (typicallyapp/google-services.json) — not the project root.
Sync NowSynchronise 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:
- The value of the API space ID field in Dotdigital.
- A class that creates a JWT token.
- A placeholder push handler class implementing the
PushMessageListenerinterface. ThePushHandleris 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 SDKInitialisation should be performed in the
onCreate()of yourApplicationsubclass 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 automaticallyThe Firebase Android SDK ships a
ContentProvider(com.google.firebase.provider.FirebaseInitProvider) that runs before yourApplication.onCreate(), soFirebaseAppis guaranteed to be ready by the time the Comapi SDK initialises. You do not need to callFirebaseApp.initializeApp(context)yourself unless you're using a non-defaultFirebaseOptionsconfiguration.
- Create a new instance of the
ComapiConfigclass, pass your API Space ID toapiSpaceId(...), your authentication handler toauthenticator(...), and your push handler topushMessageListener(...):
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:DEBUGandINFOnetwork logs typically include request URLs, headers, and bodies. Never ship a release build at anything aboveWARNING.
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 → hostimport com.comapi.APIConfig
config.apiConfiguration(APIConfig().proxy("http://10.0.2.2:8888")) // emulator → host-
Pass the
ComapiConfigto one of the initialisation methods:Comapi.initialiseShared(...)— the SDK stores the client as a singleton. Access it later viaComapi.getShared().Comapi.initialise(...)— non-singleton variant; you store the client yourself (e.g. in your DI container).
Comapi.getShared()after init.Comapi.getShared()andRxComapi.getShared()return the non-null singleton onceinitialiseShared(...)has been called — the client is created synchronously insideinitialiseShared(...), before the async network init completes. They only throwRuntimeExceptionif you call them beforeinitialiseShared(...)has run at all. With init inApplication.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>- 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:
- 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>- In your launcher Activity, route the
IntentfromonCreate(...)andonNewIntent(...)intoclient.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-linkACTION_VIEWintent. Either may befalseif 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 DotdigitalPlease 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 stepsNow 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.
Updated 18 days ago
