Skip to content
Tolinku
Tolinku
Sign In Start Free
Deep Linking · · 6 min read

Deferred Deep Linking on Android: Implementation Guide

By Tolinku Staff
|
Tolinku deferred deep linking dashboard screenshot for deep linking blog posts

Android has a structural advantage for deferred deep linking: the Play Install Referrer API. When a user clicks a link and is redirected to the Google Play Store, Google records a referrer string and delivers it to your app on first open. This creates a deterministic, privacy-friendly attribution channel that does not depend on advertising IDs or fingerprinting.

This guide covers the complete Android implementation from click to first-open routing, including the Play Install Referrer API, the Tolinku Android SDK, and intent handling for deferred link delivery.

How Android Deferred Deep Linking Works

The flow has three stages:

  1. Click capture. The user taps a Tolinku link in a browser, email, or ad. Tolinku records the click with its parameters (path, campaign, referral code) and redirects the user to the Play Store with a referrer parameter appended to the Play Store URL.

    Install. The user installs the app. Google Play records the referrer string.

    First open. On first launch, your app queries the Play Install Referrer API. The API returns the referrer string from step one. Your app sends it to Tolinku's server, which uses it to look up the original click and return the deep link data.

    On devices where Google Play is unavailable, or where the Install Referrer is not returned (some edge cases with direct APK installs), Tolinku falls back to probabilistic fingerprinting.

    Prerequisites

    • Android Studio Hedgehog or later
    • minSdk 21 (Android 5.0)
    • Kotlin 1.9+ or Java 11+
    • A Tolinku Appspace with a publishable API key

    Add the Tolinku Android SDK to your build.gradle.kts (app module):

    dependencies {
        implementation("com.tolinku.sdk:android:1.0.0")
    }
    

    The full SDK reference is available at tolinku.com/docs/developer/sdks/android/.

    Setting Up the Play Install Referrer API Directly

    While the Tolinku SDK handles Install Referrer internally, understanding how it works helps with debugging and edge case handling.

    Add the Install Referrer library to your dependencies:

    implementation("com.android.installreferrer:installreferrer:2.2")
    

    Create a client and query the referrer:

    import com.android.installreferrer.api.InstallReferrerClient
    import com.android.installreferrer.api.InstallReferrerStateListener
    import com.android.installreferrer.api.ReferrerDetails
    
    class InstallReferrerHelper(private val context: Context) {
    
        private var referrerClient: InstallReferrerClient? = null
    
        fun queryReferrer(onResult: (String?) -> Unit) {
            referrerClient = InstallReferrerClient.newBuilder(context).build()
            referrerClient?.startConnection(object : InstallReferrerStateListener {
                override fun onInstallReferrerSetupFinished(responseCode: Int) {
                    when (responseCode) {
                        InstallReferrerClient.InstallReferrerResponse.OK -> {
                            val details: ReferrerDetails? = referrerClient?.installReferrer
                            onResult(details?.installReferrer)
                            referrerClient?.endConnection()
                        }
                        InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED -> {
                            onResult(null)
                        }
                        InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE -> {
                            onResult(null)
                        }
                    }
                }
    
                override fun onInstallReferrerServiceDisconnected() {
                    // Optionally retry
                }
            })
        }
    }
    

    The installReferrer string is a URL-encoded query string, for example:

    utm_source=tolinku&utm_medium=link&tolk_click_id=abc123&utm_campaign=spring_promo
    

    Parse it with Uri.parse("http://example.com?" + referrerString) to extract individual parameters.

    First-Open Detection

    Store a flag in SharedPreferences to prevent running deferred link resolution on every launch:

    object InstallState {
        private const val PREFS_NAME = "tolinku_prefs"
        private const val KEY_FIRST_OPEN_RESOLVED = "first_open_resolved"
    
        fun hasResolvedFirstOpen(context: Context): Boolean {
            val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            return prefs.getBoolean(KEY_FIRST_OPEN_RESOLVED, false)
        }
    
        fun markFirstOpenResolved(context: Context) {
            val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            prefs.edit().putBoolean(KEY_FIRST_OPEN_RESOLVED, true).apply()
        }
    }
    

    Only run deferred resolution when hasResolvedFirstOpen returns false.

    Tolinku SDK Integration

    Tolinku dashboard showing route configuration for deep links

    Initialize the SDK in your Application class:

    import android.app.Application
    import com.tolinku.sdk.Tolinku
    
    class MyApplication : Application() {
        override fun onCreate() {
            super.onCreate()
            Tolinku.configure(
                context = this,
                publishableKey = "tolk_pub_your_key_here"
            )
        }
    }
    

    Register MyApplication in your AndroidManifest.xml:

    <application
        android:name=".MyApplication"
        ...>
    

    Handling First Open in Your Launch Activity

    In your launcher Activity, check for a first open and call resolveDeferred:

    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import com.tolinku.sdk.Tolinku
    import com.tolinku.sdk.model.TolinkuDeepLink
    
    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            if (!InstallState.hasResolvedFirstOpen(this)) {
                resolveFirstOpen()
            } else {
                // Normal app launch, handle regular deep link intents
                handleIncomingIntent(intent)
            }
        }
    
        private fun resolveFirstOpen() {
            Tolinku.shared.resolveDeferred(this) { result ->
                runOnUiThread {
                    InstallState.markFirstOpenResolved(this)
                    result.onSuccess { deepLink ->
                        routeTo(deepLink)
                    }.onFailure {
                        // No matching click found, proceed normally
                        showMainContent()
                    }
                }
            }
        }
    
        private fun routeTo(deepLink: TolinkuDeepLink) {
            // Use deepLink.path, deepLink.params, deepLink.referralCode
            when {
                deepLink.path.startsWith("/referral") -> {
                    val intent = Intent(this, ReferralOnboardingActivity::class.java).apply {
                        putExtra("referral_code", deepLink.referralCode)
                    }
                    startActivity(intent)
                    finish()
                }
                deepLink.path.startsWith("/promo") -> {
                    val intent = Intent(this, PromoActivity::class.java).apply {
                        putExtra("params", deepLink.params)
                    }
                    startActivity(intent)
                    finish()
                }
                else -> showMainContent()
            }
        }
    
        private fun showMainContent() {
            // Load default home screen
        }
    
        override fun onNewIntent(intent: Intent?) {
            super.onNewIntent(intent)
            intent?.let { handleIncomingIntent(it) }
        }
    
        private fun handleIncomingIntent(intent: Intent) {
            // Handle regular App Links (non-deferred) here
            val data = intent.data ?: return
            // Route based on data.path, data.getQueryParameter(), etc.
        }
    }
    

    Intent Handling on First Launch

    When the app opens via a direct App Link (not a deferred link), Android delivers the URL in the Intent. On first install, this does not happen unless the user tapped the App Link after installation. Make sure your intent handling logic is in handleIncomingIntent and is separate from your deferred resolution logic.

    The key distinction:

    • Deferred deep link: App was not installed when the link was tapped. Link data arrives via the Play Install Referrer or fingerprinting on the first open after install.
    • Direct deep link: App is already installed when the link is tapped. Android delivers the URL directly in the intent.

    Run deferred resolution only once (guarded by hasResolvedFirstOpen). Handle direct deep links on every launch via onNewIntent and onCreate intent inspection.

    Handling Referrer Delays

    The Play Install Referrer API is generally available immediately after install, but there can be a brief delay while the Play Store service connects. The Tolinku SDK handles retry logic internally. If you are using the API directly, implement a backoff retry:

    private fun queryWithRetry(attempt: Int = 0, onResult: (String?) -> Unit) {
        if (attempt >= 3) {
            onResult(null)
            return
        }
        referrerClient?.startConnection(object : InstallReferrerStateListener {
            override fun onInstallReferrerSetupFinished(responseCode: Int) {
                if (responseCode == InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE
                    && attempt < 2) {
                    Handler(Looper.getMainLooper()).postDelayed({
                        queryWithRetry(attempt + 1, onResult)
                    }, 1000L * (attempt + 1))
                } else {
                    // Handle result
                }
            }
            override fun onInstallReferrerServiceDisconnected() {}
        })
    }
    

    Testing Your Implementation

    Android deferred deep link testing can be done with ADB using the am broadcast:

    adb shell am broadcast \
        -a com.android.vending.INSTALL_REFERRER \
        -n your.package.name/.InstallReferrerReceiver \
        --es "referrer" "utm_source=tolinku&tolk_click_id=test123"
    

    This simulates the Play Store referrer delivery without needing a real install flow. You can test your routing logic quickly this way.

    For a complete end-to-end test:

    1. Uninstall the app from your test device.
    2. Tap a Tolinku link (from the test links section of your dashboard) on the device.
    3. Install the app via the Play Store or ADB.
    4. Open the app and verify the deferred link data arrives and routes correctly.
    5. Close and reopen. Confirm deferred resolution does not run again.

    Enable debug logging for verbose output:

    Tolinku.setLogLevel(Tolinku.LogLevel.DEBUG)
    

    Privacy and Advertising IDs

    The Google Advertising ID (GAID) is available on Android without requiring explicit permission, though users can opt out in device settings. The Play Install Referrer API does not use the GAID at all; it works through the Play Store's server-side referrer mechanism. This makes it inherently more privacy-friendly than IDFA-based attribution on iOS.

    For apps targeting children (COPPA-regulated) or apps where users have opted out of personalized ads, you should not read the GAID. The Tolinku SDK checks the opt-out status automatically and falls back to referrer-only attribution when appropriate.

    Google's Privacy Sandbox for Android will eventually change parts of this landscape, but the Play Install Referrer API is expected to remain as a first-party attribution mechanism.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.