Supporting Push Notifications
Track user interactions with push notifications to measure re-engagement campaigns and attribute conversions accurately by integrating Firebase Cloud Messaging (FCM) with the Singular SDK.
Follow the implementation guidelines below to ensure notification data is correctly passed to the Singular SDK for proper attribution.
Why Track Push Notifications: Push notifications drive re-engagement, but tracking requires correct integration. Singular ensures users who interact with notifications are properly attributed, optimizing marketing campaigns and engagement strategies.
Implementation Guide
Handle FCM Notifications
Override the
onMessageReceived()
method in your FirebaseMessagingService
to capture notification data when push messages arrive.
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
var title = ""
var body = ""
message.notification?.let {
Log.d("singular-app", it.toString())
title = it.title ?: ""
body = it.body ?: ""
}
val data: Map<String, String> = message.data
if (data.isNotEmpty()) {
Log.d("singular-app", data.toString())
}
// Forward payload data to intent
processNotification(title, body, data)
}
@Override
public void onMessageReceived(@NonNull RemoteMessage message) {
super.onMessageReceived(message);
String title = "";
String body = "";
if (message.getNotification() != null) {
Log.d("singular-app", message.getNotification().toString());
title = message.getNotification().getTitle();
body = message.getNotification().getBody();
}
Map<String, String> data = message.getData();
if (!data.isEmpty()) {
Log.d("singular-app", data.toString());
}
// Forward payload data to intent
processNotification(title, body, data);
}
Best Practice: Capture both notification content (title, body) and data payload to ensure complete tracking information is available for attribution.
Process and Forward Notification Data
Create an intent that launches your MainActivity with the notification payload attached, ensuring Singular receives the tracking data.
private fun processNotification(title: String, body: String, data: Map<String, String>) {
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
// Attach notification data to the intent
data.forEach { (key, value) ->
putExtra(key, value)
}
}
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notificationBuilder = NotificationCompat.Builder(this, "your_channel_id")
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(body)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
notificationManager?.notify(0, notificationBuilder.build())
}
private void processNotification(String title, String body, Map<String, String> data) {
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
// Attach notification data to the intent
for (Map.Entry<String, String> entry : data.entrySet()) {
intent.putExtra(entry.getKey(), entry.getValue());
}
PendingIntent pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "your_channel_id")
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(body)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setContentIntent(pendingIntent);
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager != null) {
notificationManager.notify(0, notificationBuilder.build());
}
}
Android 12+ Requirement:
Use
PendingIntent.FLAG_IMMUTABLE
for Android API 31+ to
comply with security requirements.
Configure SDK for Push Payloads
Add push notification payload selectors to your SDK configuration to specify where Singular links are located in the notification data structure.
How push attribution fits together.
The SDK does not parse incoming push payloads inside
onMessageReceived
. Instead, the link is extracted when an activity is launched from
a notification — the OS passes the notification data as the launching
intent, which you forward to
Singular.init()
via the
SingularConfig
. The lifecycle is:
-
Notification arrives →
onMessageReceiveddisplays it (your code). - User taps the notification → Android launches your activity with the notification data attached to the intent.
-
Your activity calls
Singular.init(context, config), passing the intent throughwithPushNotificationPayload(intent, selectors). - The SDK walks the selectors, finds the Singular link, and records the push-attributed session.
Push attribution depends on
Singular.init()
being reached after the activity launches from the notification.
Without
withPushNotificationPayload
configured, the link is not extracted regardless of how the
notification is built. Uninstall tracking via
setFCMDeviceToken
is independent and is not required for push attribution.
val pushSelectors = arrayOf(
arrayOf("sng_link"),
arrayOf("rootObj", "nestedObj", "anotherNested", "singularLink")
)
val config = SingularConfig("SDK_KEY", "SDK_SECRET")
.withPushNotificationPayload(intent, pushSelectors)
Singular.init(applicationContext, config)
String[][] pushSelectors = {
{"sng_link"},
{"rootObj", "nestedObj", "anotherNested", "singularLink"}
};
SingularConfig config = new SingularConfig("SDK_KEY", "SDK_SECRET")
.withPushNotificationPayload(getIntent(), pushSelectors);
Singular.init(getApplicationContext(), config);
Selector Configuration:
-
Simple Keys:
Use
arrayOf("sng_link")for top-level keys in the payload -
Nested Keys:
Use
arrayOf("rootObj", "nestedObj", "key")to traverse nested JSON structures - Multiple Paths: Define multiple selector arrays to check different possible locations for Singular links
Validation Guide
Verify Payload in Start Session
Confirm that push notification links are correctly passed to Singular by inspecting the start session API call.
The Singular SDK includes the push notification payload under the
singular_link
parameter in the start session request when
users tap notifications.
Example Start Session Request:
https://sdk-api-v1.singular.net/api/v1/start?
a=<SDK-Key>
&singular_link=https://singularassist2.sng.link/C4nw9/r1m0?_dl=singular://test&_smtype=3
&i=net.singular.singularsampleapp
&s=1740905574084
&sdk=Singular/v12.6.2
Alternative Verification: Use the Singular SDK Console to verify push notification tracking. Check the Deeplink URL field to confirm the tracking link is captured correctly.
Advanced Configuration
ESP Domain Configuration
Configure external domains if you wrap Singular links within Email Service Provider (ESP) or other third-party domains.
val config = SingularConfig("SDK_KEY", "SDK_SECRET")
.withESPDomains(listOf("sl.esp.link", "custom.domain.com"))
SingularConfig config = new SingularConfig("SDK_KEY", "SDK_SECRET")
.withESPDomains(Arrays.asList("sl.esp.link", "custom.domain.com"));
Security Note:
By default, only
sng.link
domains predefined in the Singular Manage Links page are permitted.
Configure ESP domains explicitly if using wrapped links.
Dynamic Deep Link Routing
Implement multiple deep link destinations from a single notification by configuring one Singular tracking link with dynamic redirect overrides.
Use Case Example: A breaking news notification with multiple action options
-
Read Latest News:
newsapp://article?id=12345 -
Trending Topics:
newsapp://trending -
Sports:
newsapp://sports
Instead of creating multiple tracking links, use one Singular link and override redirects dynamically based on user selection. See Overriding Redirects in Singular Tracking Links for implementation details.
Important Considerations
Implementation Notes
-
No Callback Handler:
Unlike
withSingularLink, the push notification feature does not provide payload callbacks. Implement your own deep linking logic to route users to specific content within your app -
Attribution Flow:
When users tap notifications,
Singular retrieves the payload and includes it in the start session
event triggered by
Singular.init(). The backend processes this data to attribute the push notification touchpoint and register re-engagement tracking -
Domain Restrictions:
Only Singular link domains
(
sng.link) from the Manage Links page are permitted by default. Configure ESP domains explicitly for wrapped links usingwithESPDomains()
Success: By following these steps, your app now tracks push notification interactions with Singular, improving campaign performance insights and ensuring accurate re-engagement attribution.