Implementation Reference

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.

Click-to-install attribution

Every tracking link creates a click record. When the app opens after install, it calls Link Traker and receives the original referral data.

Referral-first deferred deep linking

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.

  1. Create an account and sign in to the Link Traker dashboard.
  2. Register your Firebase app if you want automated FCM reward notifications.
  3. Create a tracking link for your APK, Play Store URL, or external destination.
  4. Share the link with a destination such as screen=ProductDetail and productId=PRODUCT_ID.
  5. Configure Android App Links so an installed app receives the HTTPS URL directly.
  6. Handle cold links in onCreate() and warm links in onNewIntent().
  7. For a new install, call /api/track-install and route using data.params.
One URL, three app states

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.

Installed and closed onCreate()

Android opens the verified HTTPS link and persists the destination before Flutter reads it.

Already running onNewIntent()

A MethodChannel sends the new destination to an app-wide Flutter listener immediately.

Not installed /api/track-install

Link Traker returns the saved custom values in data.params after the first launch.

What this file does

1
Build one dynamic URL

Append screen and the destination identifier when the user shares.

2
Verify the HTTPS domain

Configure assetlinks.json and the Android manifest so installed apps receive the URL.

3
Bridge Android to Flutter

Persist every destination and also emit warm links over a MethodChannel.

4
Route from the app root

Use the global navigator so links work from onboarding, home, or another detail screen.

Warm-link rule

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

android/app/src/main/kotlin/.../MainActivity.kt
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),
            )
        }
    }
}
Cold and warm behavior

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.

Do not scope the listener to one screen

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.

  1. Open Registered Apps in the dashboard.
  2. Click Register New App.
  3. Enter an app ID such as YOUR_APP_ID.
  4. Paste or upload your Firebase service-account JSON.
  5. Use the same appId when registering senders or tracking installs.
Security reminder

Only register apps you own. Service-account credentials are sensitive and should be managed carefully in production environments.

Create Share Links from Your App

When a user shares your app, append their referral details to the tracking link.

Step 1: Store your master tracking link

Android (strings.xml):

<string name="link_traker_master_link">https://YOUR_DOMAIN/api/t/LINK_ID</string>

Flutter (config.dart):

static const String linkTrakerMasterLink = "https://YOUR_DOMAIN/api/t/LINK_ID";

Step 2: Register the sender

Call this once when the user becomes eligible to invite friends, or refresh it whenever their FCM token changes.

POST /api/register-sender
Content-Type: application/json

{
  "referralCode": "USER_123",
  "referralUserId": "user_123",
  "referralFcmToken": "FCM_TOKEN",
  "masterLink": "https://YOUR_DOMAIN/api/t/LINK_ID",
  "appId": "YOUR_APP_ID"
}

Step 3: Share with referral parameters

Add the destination screen and its identifier to the same tracking URL. Link Traker stores these custom parameters and returns them after installation.

Kotlin Example:

val referralCode = "USER_123"
val productId = "PRODUCT_123"
val baseLink = getString(R.string.link_traker_master_link)
val finalLink = Uri.parse(baseLink).buildUpon()
    .appendQueryParameter("ref", referralCode)
    .appendQueryParameter("screen", "ProductDetail")
    .appendQueryParameter("productId", productId)
    .build()
    .toString()

val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_TEXT, "Join me and unlock your reward: $finalLink")
}
startActivity(Intent.createChooser(intent, "Invite via"))

Flutter Example:

import 'package:share_plus/share_plus.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

Future<void> shareProduct({
  required String userCode,
  required String productId,
}) async {
  final fcmToken = await FirebaseMessaging.instance.getToken();
  final link = Uri.parse(AppConfig.linkTrakerMasterLink).replace(
    queryParameters: {
      'ref': userCode,
      'screen': 'ProductDetail',
      'productId': productId,
      if (fcmToken != null) 'referralFcmToken': fcmToken,
    },
  );

  await Share.share("Check out this product: $link");
}

Step 4: Record the share CTA

Immediately before opening the native share sheet, send one idempotent share event. Reuse the same eventId if the request is retried. This powers the selected link's User Analytics rankings for most-shared products, screens, and users.

POST /api/track-share
Content-Type: application/json

{
  "eventId": "STABLE_UUID_FOR_THIS_SHARE",
  "linkId": "LINK_ID",
  "appId": "YOUR_APP_ID",
  "referralCode": "USER_123",
  "referralUserId": "user_123",
  "referralFcmToken": "CURRENT_FCM_TOKEN",
  "platform": "android",
  "screen": "ProductDetail",
  "targetId": "PRODUCT_123",
  "params": {
    "screen": "ProductDetail",
    "productId": "PRODUCT_123",
    "productName": "Blue Shoe"
  }
}
FCM tokens are not stored in share events

The token refreshes the sender registry for push delivery. Analytics records keep only the user, product, screen, app, and share timestamp.

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.

Required destination values

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>
Required for direct installed-app opening

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"
      ]
    }
  }
]
Use the certificate that signs the installed build

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.
}
Use both storage and MethodChannel delivery

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.

deep_link_service.dart
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;
    }
  }
}
main.dart
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"
}
MainActivity.kt - read the Play install referrer
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)
    }
}
Consume deferred attribution once

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"
Expected result

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"
}
Enable user-to-user activity history

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.

First-install opens are automatic

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 cid from 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 appId is used in links, sender registration, and install tracking.

Install count is not increasing

  • Call /api/track-install once after first app launch.
  • Verify the app has internet access during onboarding.
  • Check the analytics modal for click records and attribution method.