Developer Integration Guide
How Link Traker Works
Link Traker helps developers create trackable app links, measure clicks, downloads, and installs, then return referral data to the app after first launch.
Every tracking link creates a click record. When the app opens after install, it calls Link Traker and receives the original referral data.
Use the returned referralCode, linkId, and attribution method
to open the right referral, onboarding, reward, or campaign screen inside your app.
Quick Start
Use this flow when a shared link must open the exact screen whether the app is running, closed, or not installed yet.
- Create an account and sign in to the Link Traker dashboard.
- Register your Firebase app if you want automated FCM reward notifications.
- Create a tracking link for your APK, Play Store URL, or external destination.
- Share the link with a destination such as
screen=ProductDetailandproductId=PRODUCT_ID. - Configure Android App Links so an installed app receives the HTTPS URL directly.
- Handle cold links in
onCreate()and warm links inonNewIntent(). - For a new install, call
/api/track-installand route usingdata.params.
The same Link Traker URL supports an already-running app, a cold app launch, and a first launch after installation.
Deferred Deep Linking Architecture
A complete integration must handle three states. Link Traker preserves the same destination parameters across each state so the user reaches the requested screen instead of only the app home page.
Android opens the verified HTTPS link and persists the destination before Flutter reads it.
A MethodChannel sends the new destination to an app-wide Flutter
listener immediately.
Link Traker returns the saved custom values in data.params after the
first launch.
What this file does
Append screen and the destination identifier when the user shares.
Configure assetlinks.json and the Android manifest so installed apps
receive the URL.
Persist every destination and also emit warm links over a
MethodChannel.
Use the global navigator so links work from onboarding, home, or another detail screen.
SharedPreferences alone is not enough while the app remains foregrounded. Android
may deliver onNewIntent() without a Flutter lifecycle resume, so the
live MethodChannel is required.
Native delivery contract
class MainActivity : FlutterActivity() {
companion object {
private const val CHANNEL = "link_traker/deep_links"
}
private lateinit var deepLinkChannel: MethodChannel
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
deepLinkChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
CHANNEL,
)
}
// Cold link: persist the destination before Flutter reads it.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleDeepLink(appState = "cold")
}
// Warm link: singleTop delivers the new URL here.
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleDeepLink(appState = "warm")
}
private fun handleDeepLink(appState: String) {
val uri = intent?.data ?: return
val supported = uri.scheme == "https" &&
uri.host == "YOUR_DOMAIN" &&
uri.path?.startsWith("/api/t/") == true
if (!supported) return
val linkId = uri.pathSegments.lastOrNull() ?: return
val screen = uri.getQueryParameter("screen") ?: return
val targetId = uri.getQueryParameter("productId")
?: uri.getQueryParameter("targetId")
?: return
val referralCode = uri.getQueryParameter("ref")
?: uri.getQueryParameter("referralCode")
deliverDeepLink(screen, targetId)
trackDeepLinkOpen(linkId, appState, screen, targetId, referralCode)
intent.data = null
}
private fun deliverDeepLink(screen: String, targetId: String) {
val prefs = getSharedPreferences(
"FlutterSharedPreferences",
Context.MODE_PRIVATE,
)
prefs.edit()
.putString("flutter.deep_link_screen", screen)
.putString("flutter.deep_link_target_id", targetId)
.apply()
if (::deepLinkChannel.isInitialized) {
deepLinkChannel.invokeMethod(
"openDeepLink",
mapOf("screen" to screen, "targetId" to targetId),
)
}
}
}
onCreate() prevents stale data during startup. onNewIntent()
plus the channel switches screens immediately while the app remains open. Send the
same stable event ID when retrying trackDeepLinkOpen() so analytics are
recorded once.
The Flutter channel listener belongs at app startup. A listener inside only the home or shell screen will miss links received while onboarding or another route is open.
Dashboard Setup
The dashboard is where you manage links and review attribution performance.
Create a link
- Open the dashboard and click Create Tracking Link.
- Upload a file or APK, or point the link to an existing app/store URL.
- Choose a custom link ID if you want a human-readable campaign link.
- Select a registered app when this link should use app-specific FCM notifications.
Track performance
- Open User Analytics from a tracking link row or its detail page to view only that link's users, products, screens, and push-enabled devices.
- Select the history action beside a user to see links they shared, engagement those links generated, and links they received or opened.
- Clicks: browser visits recorded for the tracking URL.
- Downloads: download actions or direct store redirects.
- Installs: app launches attributed through the install endpoint.
- Deep link opens: cold, warm, and first-install destination opens.
- Drop-off: downloads that have not yet become installs.
Register Your App for Notifications
Registering an app lets Link Traker send reward notifications through your Firebase project.
- Open Registered Apps in the dashboard.
- Click Register New App.
- Enter an app ID such as
YOUR_APP_ID. - Paste or upload your Firebase service-account JSON.
- Use the same
appIdwhen registering senders or tracking installs.
Only register apps you own. Service-account credentials are sensitive and should be managed carefully in production environments.
Android & Flutter Integration
Complete each step below for direct installed-app routing and deferred routing after a new installation. Skipping the warm-link channel will make links appear to work only after an app restart.
Use a stable screen key such as ProductDetail and a target identifier
such as productId. Your app decides how those values map to routes.
Step 1: Add the verified App Link intent filter
Edit android/app/src/main/AndroidManifest.xml. Keep the launcher filter and add
the verified HTTPS filter to the same MainActivity.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application ...>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="YOUR_DOMAIN"
android:pathPrefix="/api/t/" />
</intent-filter>
</activity>
</application>
</manifest>
Without a verified App Link, Android may keep the tracking URL in the browser even when the app is installed.
Step 2: Publish assetlinks.json
Your Link Traker domain must serve this file at
https://YOUR_DOMAIN/.well-known/assetlinks.json with no redirect.
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "YOUR_PACKAGE_NAME",
"sha256_cert_fingerprints": [
"YOUR_DEBUG_OR_RELEASE_SHA256",
"YOUR_PLAY_APP_SIGNING_SHA256"
]
}
}
]
Debug, local release, and Google Play builds may use different SHA-256 fingerprints. Include every certificate that should open the domain.
Step 3: Deliver cold and warm links from MainActivity
Use the same native delivery function for both Android lifecycle paths. Handle the launch
intent in onCreate(), then handle every new link in
onNewIntent(). The activity must use singleTop.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleDeepLink() // App was closed.
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleDeepLink() // App is already running.
}
Store the destination for cold-start recovery, then invoke
link_traker/deep_links for immediate warm-app navigation. The complete
MainActivity.kt contract is shown in the Deferred Deep Linking section
above.
Step 4: Add one app-wide Flutter listener
Register the channel before runApp() and route with a global navigator key. Keep
the listener above individual screens so it remains active when the user navigates around
your app.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
final navigatorKey = GlobalKey<NavigatorState>();
class DeepLinkService {
DeepLinkService._();
static final instance = DeepLinkService._();
static const _channel = MethodChannel('link_traker/deep_links');
bool _appReady = false;
bool _handling = false;
void initialize() {
_channel.setMethodCallHandler((call) async {
if (call.method != 'openDeepLink' || !_appReady) return;
final args = Map<String, dynamic>.from(call.arguments as Map);
await _open(
screen: args['screen'] as String?,
targetId: args['targetId'] as String?,
);
});
}
Future<void> markAppReady() async {
_appReady = true;
await openPendingLink();
}
Future<void> openPendingLink() async {
if (!_appReady || _handling) return;
final prefs = await SharedPreferences.getInstance();
await _open(
screen: prefs.getString('deep_link_screen'),
targetId: prefs.getString('deep_link_target_id'),
);
}
Future<void> _open({String? screen, String? targetId}) async {
final navigator = navigatorKey.currentState;
if (screen == null || screen.isEmpty || navigator == null || _handling) return;
_handling = true;
try {
navigator.pushNamed(
'/deep-link',
arguments: {'screen': screen, 'targetId': targetId},
);
final prefs = await SharedPreferences.getInstance();
await prefs.remove('deep_link_screen');
await prefs.remove('deep_link_target_id');
} finally {
_handling = false;
}
}
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
DeepLinkService.instance.initialize();
runApp(const MyApp());
}
MaterialApp(
navigatorKey: navigatorKey,
onGenerateRoute: AppRouter.onGenerateRoute,
)
// Call after authentication, onboarding, and splash routing are complete.
await DeepLinkService.instance.markAppReady();
Step 5: Recover the destination after installation
If the app was not installed when the link was clicked, recover the click identity on first
launch and call /api/track-install once. For Google Play distribution, use the
Play Install Referrer API. A clipboard payload can remain a fallback for direct APK
distribution, but it should not be your primary installed-app routing mechanism. Call
recoverDeferredLink() only while the local first-install flag is still pending.
dependencies {
implementation "com.android.installreferrer:installreferrer:2.2"
}
import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerStateListener
private fun recoverDeferredLink() {
val client = InstallReferrerClient.newBuilder(this).build()
client.startConnection(object : InstallReferrerStateListener {
override fun onInstallReferrerSetupFinished(responseCode: Int) {
if (responseCode != InstallReferrerClient.InstallReferrerResponse.OK) {
client.endConnection()
return
}
try {
val value = client.installReferrer.installReferrer
if (!value.contains("lt_cid_") || !value.contains("__lid_")) return
val cid = value.substringAfter("lt_cid_").substringBefore("__lid_")
val linkId = value.substringAfter("__lid_").substringBefore("__ref_")
// Send cid and linkId to /api/track-install once.
trackInstall(cid = cid, linkId = linkId)
} finally {
client.endConnection()
}
}
override fun onInstallReferrerServiceDisconnected() = Unit
})
}
POST https://YOUR_DOMAIN/api/track-install
Content-Type: application/json
{
"cid": "CLICK_DOC_ID",
"linkId": "LINK_ID",
"appId": "YOUR_APP_ID",
"openedByUserId": "CURRENT_APP_USER_ID",
"openedByFcmToken": "CURRENT_DEVICE_FCM_TOKEN",
"osName": "Android",
"osVersion": "14"
}
// Use the returned destination.
{
"success": true,
"data": {
"params": {
"screen": "ProductDetail",
"productId": "PRODUCT_1"
}
}
}
val response = JSONObject(responseBody)
val params = response
.optJSONObject("data")
?.optJSONObject("params")
val screen = params?.optString("screen")
val targetId = params?.optString("productId")
?.takeIf { it.isNotBlank() }
?: params?.optString("targetId")?.takeIf { it.isNotBlank() }
if (!screen.isNullOrBlank() && !targetId.isNullOrBlank()) {
runOnUiThread {
deliverDeepLink(screen, targetId)
}
}
Persist a local completion flag after a successful first-install attribution call. Clear the stored destination after navigation to prevent duplicate screens, installs, or rewards.
Step 6: Verify all three app states
Test with two different target IDs so a stale destination cannot look like a successful warm link.
# Confirm Android verified your domain.
adb shell pm get-app-links YOUR_PACKAGE_NAME
# Cold app: should open PRODUCT_1.
adb shell am force-stop YOUR_PACKAGE_NAME
adb shell am start -a android.intent.action.VIEW \
-d "https://YOUR_DOMAIN/api/t/LINK_ID?screen=ProductDetail&productId=PRODUCT_1"
# Warm app: should switch immediately to PRODUCT_2 without restarting.
adb shell am start -a android.intent.action.VIEW \
-d "https://YOUR_DOMAIN/api/t/LINK_ID?screen=ProductDetail&productId=PRODUCT_2"
The closed app opens PRODUCT_1; the running app immediately changes to
PRODUCT_2; and a first install opens the destination returned in
data.params. Open User Analytics for the tracking link,
then select a user to inspect the related share, click, and app-open history.
API Reference
Dashboard management APIs require a Firebase bearer token from the signed-in dashboard user. Mobile attribution and referral APIs are called from your app.
Create tracking link
POST /api/links
Authorization: Bearer FIREBASE_ID_TOKEN
{
"linkId": "SUMMER_OFFER",
"targetUrl": "https://play.google.com/store/apps/details?id=com.example.app",
"directRedirect": true,
"appId": "YOUR_APP_ID"
}
Register sender
POST /api/register-sender
Use this to store the referrer's user ID and FCM token before they share links.
{
"referralCode": "USER_123",
"referralUserId": "user_123",
"referralFcmToken": "FCM_TOKEN",
"masterLink": "https://YOUR_DOMAIN/api/t/LINK_ID",
"appId": "YOUR_APP_ID"
}
Tracking link
GET /api/t/:linkId
Records the click and redirects the visitor to the download page, file, or store URL. Any extra query parameters (e.g. screen, productId) are automatically saved and returned later by the install attribution response.
https://YOUR_DOMAIN/api/t/MAIN_LINK?ref=USER_123
https://YOUR_DOMAIN/api/t/MAIN_LINK?screen=ProductDetail&productId=shoes_123
Install attribution
POST /api/track-install
Call this from your app on first launch. Direct cid matching is preferred. If no
click ID is available, Link Traker attempts fingerprint matching.
// Request Body
{
"cid": "CLICK_DOC_ID",
"linkId": "LINK_ID",
"referralCode": "USER_123",
"openedByUserId": "new_user_456",
"openedByFcmToken": "RECIPIENT_FCM_TOKEN",
"appId": "YOUR_APP_ID"
}
// Response JSON (200 OK)
{
"success": true,
"method": "clipboard" | "fingerprint" | "manual_referral",
"referralCode": "REFERRER_ID",
"data": {
"clickId": "CLICK_OR_SESSION_ID",
"customReferrer": "REFERRER_ID",
"referralFcmToken": "REFERRER_FCM_TOKEN",
"referralUserId": "REFERRER_USER_ID",
"is_install": true,
"params": {
"screen": "ProductDetail",
"productId": "shoes_123"
}
}
}
Track an installed-app deep-link open
POST /api/track-deep-link-open
Call this when Android or iOS delivers a tracking URL directly to an installed app. Generate one event ID per received URL and reuse it for network retries.
{
"eventId": "STABLE_UUID_FOR_THIS_OPEN",
"linkId": "LINK_ID",
"appId": "YOUR_APP_ID",
"eventType": "direct_open",
"appState": "cold",
"source": "app_link",
"platform": "android",
"screen": "ProductDetail",
"targetId": "PRODUCT_123",
"referralCode": "USER_123",
"openedByUserId": "CURRENT_APP_USER_ID",
"openedByFcmToken": "CURRENT_DEVICE_FCM_TOKEN",
"params": {
"screen": "ProductDetail",
"productId": "PRODUCT_123"
},
"osVersion": "14",
"appVersion": "1.0.0"
}
Send openedByUserId whenever your app knows the signed-in recipient.
openedByFcmToken is optional and is hashed before storage; the raw token
and IP address are never returned by the analytics API. Without recipient identity,
Link Traker still records engagement for the sender but labels the recipient unknown.
When /api/track-install returns a saved destination, Link Traker records
a deferred_open event automatically. The explicit endpoint closes the
analytics gap for installed cold and warm App Links.
Track a share CTA
POST /api/track-share
Call this immediately before showing the native share sheet. It records a true share for
that tracking link's product, screen, and user rankings. Send a stable eventId,
linkId, referralCode, screen, and optional product
parameters.
Validate referral
POST /api/validate-referral
{
"referralCode": "USER_123"
}
Notify referrer
POST /api/notify-referral
{
"referralCode": "USER_123",
"rewardDays": 15,
"friendName": "A friend",
"appId": "YOUR_APP_ID"
}
Troubleshooting
No referral code returned
- Confirm the shared URL includes
?ref=USER_CODE. - Confirm your app sends the correct
linkId. - Use the exact
cidfrom the clipboard or Play Install Referrer when available.
Notifications are not delivered
- Register the app in the dashboard with a valid Firebase service account.
- Send the current sender FCM token using
/api/register-sender. - Make sure the same
appIdis used in links, sender registration, and install tracking.
Install count is not increasing
- Call
/api/track-installonce after first app launch. - Verify the app has internet access during onboarding.
- Check the analytics modal for click records and attribution method.