{"id":586,"date":"2026-03-24T09:00:00","date_gmt":"2026-03-24T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=586"},"modified":"2026-03-07T03:32:55","modified_gmt":"2026-03-07T08:32:55","slug":"deferred-deep-linking-android","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/deferred-deep-linking-android\/","title":{"rendered":"Deferred Deep Linking on Android: Implementation Guide"},"content":{"rendered":"\n<p>Android has a structural advantage for deferred deep linking: the <a href=\"https:\/\/developer.android.com\/google\/play\/installreferrer\" rel=\"nofollow noopener\" target=\"_blank\">Play Install Referrer API<\/a>. 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.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How Android Deferred Deep Linking Works<\/h2>\n\n\n\n<p>The flow has three stages:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><p><strong>Click capture.<\/strong> 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.<\/p>\n\n\n\n<p><strong>Install.<\/strong> The user installs the app. Google Play records the referrer string.<\/p>\n\n\n\n<p><strong>First open.<\/strong> 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&#39;s server, which uses it to look up the original click and return the deep link data.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Android Studio Hedgehog or later<\/li>\n<li>minSdk 21 (Android 5.0)<\/li>\n<li>Kotlin 1.9+ or Java 11+<\/li>\n<li>A Tolinku Appspace with a publishable API key<\/li>\n<\/ul>\n\n\n\n<p>Add the Tolinku Android SDK to your <code>build.gradle.kts<\/code> (app module):<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">dependencies {\n    implementation(&quot;com.tolinku.sdk:android:1.0.0&quot;)\n}\n<\/code><\/pre>\n\n\n\n<p>The full SDK reference is available at <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/android\/\">tolinku.com\/docs\/developer\/sdks\/android\/<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Setting Up the Play Install Referrer API Directly<\/h2>\n\n\n\n<p>While the Tolinku SDK handles Install Referrer internally, understanding how it works helps with debugging and edge case handling.<\/p>\n\n\n\n<p>Add the Install Referrer library to your dependencies:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">implementation(&quot;com.android.installreferrer:installreferrer:2.2&quot;)\n<\/code><\/pre>\n\n\n\n<p>Create a client and query the referrer:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">import com.android.installreferrer.api.InstallReferrerClient\nimport com.android.installreferrer.api.InstallReferrerStateListener\nimport com.android.installreferrer.api.ReferrerDetails\n\nclass InstallReferrerHelper(private val context: Context) {\n\n    private var referrerClient: InstallReferrerClient? = null\n\n    fun queryReferrer(onResult: (String?) -&gt; Unit) {\n        referrerClient = InstallReferrerClient.newBuilder(context).build()\n        referrerClient?.startConnection(object : InstallReferrerStateListener {\n            override fun onInstallReferrerSetupFinished(responseCode: Int) {\n                when (responseCode) {\n                    InstallReferrerClient.InstallReferrerResponse.OK -&gt; {\n                        val details: ReferrerDetails? = referrerClient?.installReferrer\n                        onResult(details?.installReferrer)\n                        referrerClient?.endConnection()\n                    }\n                    InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED -&gt; {\n                        onResult(null)\n                    }\n                    InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE -&gt; {\n                        onResult(null)\n                    }\n                }\n            }\n\n            override fun onInstallReferrerServiceDisconnected() {\n                \/\/ Optionally retry\n            }\n        })\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>The <code>installReferrer<\/code> string is a URL-encoded query string, for example:<\/p>\n\n\n\n<pre><code>utm_source=tolinku&amp;utm_medium=link&amp;tolk_click_id=abc123&amp;utm_campaign=spring_promo\n<\/code><\/pre>\n\n\n\n<p>Parse it with <code>Uri.parse(&quot;http:\/\/example.com?&quot; + referrerString)<\/code> to extract individual parameters.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">First-Open Detection<\/h2>\n\n\n\n<p>Store a flag in <code>SharedPreferences<\/code> to prevent running deferred link resolution on every launch:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">object InstallState {\n    private const val PREFS_NAME = &quot;tolinku_prefs&quot;\n    private const val KEY_FIRST_OPEN_RESOLVED = &quot;first_open_resolved&quot;\n\n    fun hasResolvedFirstOpen(context: Context): Boolean {\n        val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)\n        return prefs.getBoolean(KEY_FIRST_OPEN_RESOLVED, false)\n    }\n\n    fun markFirstOpenResolved(context: Context) {\n        val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)\n        prefs.edit().putBoolean(KEY_FIRST_OPEN_RESOLVED, true).apply()\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>Only run deferred resolution when <code>hasResolvedFirstOpen<\/code> returns <code>false<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku SDK Integration<\/h2>\n\n\n\n<p><img decoding=\"async\" src=\"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/platform-platform-routes.png\" alt=\"Tolinku dashboard showing route configuration for deep links\"><\/p>\n\n\n\n<p>Initialize the SDK in your <code>Application<\/code> class:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">import android.app.Application\nimport com.tolinku.sdk.Tolinku\n\nclass MyApplication : Application() {\n    override fun onCreate() {\n        super.onCreate()\n        Tolinku.configure(\n            context = this,\n            publishableKey = &quot;tolk_pub_your_key_here&quot;\n        )\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>Register <code>MyApplication<\/code> in your <code>AndroidManifest.xml<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;application\n    android:name=&quot;.MyApplication&quot;\n    ...&gt;\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Handling First Open in Your Launch Activity<\/h2>\n\n\n\n<p>In your launcher <code>Activity<\/code>, check for a first open and call <code>resolveDeferred<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">import androidx.appcompat.app.AppCompatActivity\nimport android.os.Bundle\nimport com.tolinku.sdk.Tolinku\nimport com.tolinku.sdk.model.TolinkuDeepLink\n\nclass MainActivity : AppCompatActivity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_main)\n\n        if (!InstallState.hasResolvedFirstOpen(this)) {\n            resolveFirstOpen()\n        } else {\n            \/\/ Normal app launch, handle regular deep link intents\n            handleIncomingIntent(intent)\n        }\n    }\n\n    private fun resolveFirstOpen() {\n        Tolinku.shared.resolveDeferred(this) { result -&gt;\n            runOnUiThread {\n                InstallState.markFirstOpenResolved(this)\n                result.onSuccess { deepLink -&gt;\n                    routeTo(deepLink)\n                }.onFailure {\n                    \/\/ No matching click found, proceed normally\n                    showMainContent()\n                }\n            }\n        }\n    }\n\n    private fun routeTo(deepLink: TolinkuDeepLink) {\n        \/\/ Use deepLink.path, deepLink.params, deepLink.referralCode\n        when {\n            deepLink.path.startsWith(&quot;\/referral&quot;) -&gt; {\n                val intent = Intent(this, ReferralOnboardingActivity::class.java).apply {\n                    putExtra(&quot;referral_code&quot;, deepLink.referralCode)\n                }\n                startActivity(intent)\n                finish()\n            }\n            deepLink.path.startsWith(&quot;\/promo&quot;) -&gt; {\n                val intent = Intent(this, PromoActivity::class.java).apply {\n                    putExtra(&quot;params&quot;, deepLink.params)\n                }\n                startActivity(intent)\n                finish()\n            }\n            else -&gt; showMainContent()\n        }\n    }\n\n    private fun showMainContent() {\n        \/\/ Load default home screen\n    }\n\n    override fun onNewIntent(intent: Intent?) {\n        super.onNewIntent(intent)\n        intent?.let { handleIncomingIntent(it) }\n    }\n\n    private fun handleIncomingIntent(intent: Intent) {\n        \/\/ Handle regular App Links (non-deferred) here\n        val data = intent.data ?: return\n        \/\/ Route based on data.path, data.getQueryParameter(), etc.\n    }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Intent Handling on First Launch<\/h2>\n\n\n\n<p>When the app opens via a direct App Link (not a deferred link), Android delivers the URL in the <code>Intent<\/code>. On first install, this does not happen unless the user tapped the App Link after installation. Make sure your intent handling logic is in <code>handleIncomingIntent<\/code> and is separate from your deferred resolution logic.<\/p>\n\n\n\n<p>The key distinction:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Deferred deep link<\/strong>: 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.<\/li>\n<li><strong>Direct deep link<\/strong>: App is already installed when the link is tapped. Android delivers the URL directly in the intent.<\/li>\n<\/ul>\n\n\n\n<p>Run deferred resolution only once (guarded by <code>hasResolvedFirstOpen<\/code>). Handle direct deep links on every launch via <code>onNewIntent<\/code> and <code>onCreate<\/code> intent inspection.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Handling Referrer Delays<\/h2>\n\n\n\n<p>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:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">private fun queryWithRetry(attempt: Int = 0, onResult: (String?) -&gt; Unit) {\n    if (attempt &gt;= 3) {\n        onResult(null)\n        return\n    }\n    referrerClient?.startConnection(object : InstallReferrerStateListener {\n        override fun onInstallReferrerSetupFinished(responseCode: Int) {\n            if (responseCode == InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE\n                &amp;&amp; attempt &lt; 2) {\n                Handler(Looper.getMainLooper()).postDelayed({\n                    queryWithRetry(attempt + 1, onResult)\n                }, 1000L * (attempt + 1))\n            } else {\n                \/\/ Handle result\n            }\n        }\n        override fun onInstallReferrerServiceDisconnected() {}\n    })\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Testing Your Implementation<\/h2>\n\n\n\n<p>Android deferred deep link testing can be done with ADB using the <code>am<\/code> broadcast:<\/p>\n\n\n\n<pre><code class=\"language-bash\">adb shell am broadcast \\\n    -a com.android.vending.INSTALL_REFERRER \\\n    -n your.package.name\/.InstallReferrerReceiver \\\n    --es &quot;referrer&quot; &quot;utm_source=tolinku&amp;tolk_click_id=test123&quot;\n<\/code><\/pre>\n\n\n\n<p>This simulates the Play Store referrer delivery without needing a real install flow. You can test your routing logic quickly this way.<\/p>\n\n\n\n<p>For a complete end-to-end test:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Uninstall the app from your test device.<\/li>\n<li>Tap a Tolinku link (from the test links section of your dashboard) on the device.<\/li>\n<li>Install the app via the Play Store or ADB.<\/li>\n<li>Open the app and verify the deferred link data arrives and routes correctly.<\/li>\n<li>Close and reopen. Confirm deferred resolution does not run again.<\/li>\n<\/ol>\n\n\n\n<p>Enable debug logging for verbose output:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">Tolinku.setLogLevel(Tolinku.LogLevel.DEBUG)\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Privacy and Advertising IDs<\/h2>\n\n\n\n<p>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&#39;s server-side referrer mechanism. This makes it inherently more privacy-friendly than IDFA-based attribution on iOS.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>Google&#39;s <a href=\"https:\/\/developer.android.com\/design-for-safety\/privacy-sandbox\" rel=\"nofollow noopener\" target=\"_blank\">Privacy Sandbox for Android<\/a> will eventually change parts of this landscape, but the Play Install Referrer API is expected to remain as a first-party attribution mechanism.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Related Resources<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/android\/\">Tolinku Android SDK documentation<\/a><\/li>\n<li><a href=\"https:\/\/tolinku.com\/docs\/developer\/quick-start\/\">Quick start guide<\/a><\/li>\n<li><a href=\"https:\/\/tolinku.com\/docs\/concepts\/deferred-deep-linking\/\">Deferred deep linking concepts<\/a><\/li>\n<li><a href=\"https:\/\/tolinku.com\/blog\/google-play-install-referrer\/\">Google Play Install Referrer: deep dive<\/a><\/li>\n<li><a href=\"https:\/\/developer.android.com\/training\/app-links\" rel=\"nofollow noopener\" target=\"_blank\">Android App Links documentation<\/a><\/li>\n<li><a href=\"https:\/\/developer.android.com\/google\/play\/installreferrer\/library\" rel=\"nofollow noopener\" target=\"_blank\">Play Install Referrer Library<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Android&#8217;s Play Install Referrer API makes deferred deep linking more reliable than on iOS. This guide covers InstallReferrerClient setup, first-open intent handling, and Tolinku Android SDK integration in Kotlin.<\/p>\n","protected":false},"author":2,"featured_media":585,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Deferred Deep Linking on Android: Kotlin Implementation Guide","rank_math_description":"Implement deferred deep linking on Android with Kotlin. Covers Play Install Referrer API, first-open detection, Tolinku Android SDK, and intent handling.","rank_math_focus_keyword":"deferred deep linking Android","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-deferred-deep-linking-android.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-deferred-deep-linking-android.png","footnotes":""},"categories":[11],"tags":[25,23,28,21,34,108],"class_list":["post-586","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-deep-linking","tag-android","tag-app-links","tag-attribution","tag-deferred-deep-linking","tag-kotlin","tag-play-install-referrer"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/586","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=586"}],"version-history":[{"count":1,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/586\/revisions"}],"predecessor-version":[{"id":587,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/586\/revisions\/587"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/585"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=586"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=586"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=586"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}