Skip to main content

Handling opens and deep links

Every SDK normalises a press into one message and reports the event before your handler runs. Your job is to route.

The message, again

FieldUse it for
urlThe deep link. If a button with its own url was pressed, this is the button's.
dataThe campaign's data map — screen, ids, anything you set. Flat strings only (FCM data allows nothing else).
actionIdWhich button, when any.
msgId, campaignIdAnalytics correlation.

Prefer data.screen + ids for in-app navigation and url for links that may leave the app or be shared.

Routing patterns

React Native

OpenNotification.onOpen(({ url, data, actionId }) => {
if (actionId === "later") return; // a "remind me" button
if (data.screen) navigation.navigate(data.screen, data);
else if (url) Linking.openURL(url);
});

Swift

OpenNotification.shared.onOpen = { message in
if let screen = message.data["screen"] { router.show(screen, params: message.data) }
else if let url = message.url.flatMap(URL.init) { UIApplication.shared.open(url) }
}

Kotlin

OpenNotification.onOpen = { message ->
message.data["screen"]?.let { router.show(it, message.data) }
?: message.url?.let { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it))) }
}

Web — the service worker opens data.url (or the button's data["a:<id>"]), focusing an existing tab on that path first. To handle it in-page, put what you need in the URL's query string and read it on load.

Cold starts

PlatformHow the press reaches you
iOS (Swift)handleLaunch(userInfo:) in didFinishLaunching — replayed into onOpen once it is set.
Android (Kotlin)OpenNotification.messageFrom(intent) in onCreate / onNewIntent.
React NativeHeld natively until JS is ready, then delivered to onOpen.
WebThe worker opens/focuses the URL; the page loads normally.

Action buttons

Defined per locale in the content (actions: [{ id, title, url? }], max 3). How each platform shows them:

Buttons shownWhere the button's URL goes
iOSup to 4 (we send ≤ 3); needs the NSE to register the categorymessage.url
Android3message.url
Chrome / Edge2data["a:<id>"]
Safarinone

A button press reports clicked, a body press opened. Both count toward the funnel; only clicked counts as CTR.

Foreground arrivals

onReceive fires when a push lands while the app is open. The SDKs still show the system notification (iOS via willPresent, Android by drawing it); use onReceive to refresh a badge or a list, not to show a second alert.

The tracking endpoint

The SDKs call it for you; documented here for custom integrations:

POST /v1/e/d/<msgId> delivered
POST /v1/e/o/<msgId> opened
POST /v1/e/c/<msgId> clicked
POST /v1/e/x/<msgId> dismissed

No headers, no body, no key. The msgId is HMAC-signed and embeds appId|campaignId|userId|subscriptionId|platform|variant, so the API verifies it without a database read, writes the event, bumps the campaign (and variant) counters and emits the webhook. Rate-limited per IP (EVENT_RATE_LIMIT). A replayed ping only moves analytics counters; it grants nothing.