Deferred Deep Links and Attribution

AndroidDeep LinksAnalyticsGrowth
Share on LinkedIn

A user clicks your ad for 30% off running shoes, installs the app, and lands on the generic home screen. They bounce. That conversion — and the ad spend behind it — is lost because you didn't implement deferred deep linking. Deferred links carry the user's intent through the install gap: click → Play Store → install → open → intended content. Getting this right requires coordination between your web domain, Play Store referrer data, and in-app routing. I've debugged deferred link failures that turned out to be domain verification typos, referrer API timing issues, and routing logic that only handled warm-start deep links, not first-open.

The deferred link flow

User clicks https://example.com/shoes/sale
    ↓
Web page (or direct) → Play Store with referrer param
    ↓
User installs and opens app (first launch)
    ↓
App reads Install Referrer → extracts /shoes/sale
    ↓
NavController routes to SaleScreen

The challenge: on first launch, there's no Intent with the deep link URI. You must retrieve the referrer asynchronously and route after.

App Links setup (prerequisite)

Verify your domain for HTTPS deep links:

<!-- AndroidManifest.xml -->
<activity android:name=".MainActivity" android:exported="true">
    <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="example.com"
              android:pathPrefix="/shoes" />
    </intent-filter>
</activity>

Host assetlinks.json at https://example.com/.well-known/assetlinks.json:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.app",
    "sha256_cert_fingerprints": ["AB:CD:..."]
  }
}]

Verify with:

adb shell pm verify-app-links --re-verify com.example.app
adb shell pm get-app-links com.example.app

Without verified App Links, users see a disambiguation dialog — conversion killer.

Play Install Referrer

Read the referrer on first launch:

class InstallReferrerReader(private val context: Context) {
    suspend fun getReferrer(): String? = suspendCancellableCoroutine { cont ->
        val client = InstallReferrerClient.newBuilder(context).build()
        client.startConnection(object : InstallReferrerStateListener {
            override fun onInstallReferrerSetupFinished(code: Int) {
                if (code == InstallReferrerClient.InstallReferrerResponse.OK) {
                    val referrer = client.installReferrer.installReferrer
                    client.endConnection()
                    cont.resume(parseReferrerUrl(referrer))
                } else {
                    client.endConnection()
                    cont.resume(null)
                }
            }
            override fun onInstallReferrerServiceDisconnected() {
                cont.resume(null)
            }
        })
    }

    private fun parseReferrerUrl(referrer: String): String? {
        // referrer format: utm_source=...&deep_link=https%3A%2F%2Fexample.com%2Fshoes%2Fsale
        return Uri.parse("?$referrer").getQueryParameter("deep_link")
    }
}

Call once on first launch, persist the result, and route:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Handle direct deep link (app already installed)
        intent?.data?.let { uri -> handleDeepLink(uri); return }

        // Handle deferred deep link (first install)
        if (isFirstLaunch()) {
            lifecycleScope.launch {
                val deferredUri = InstallReferrerReader(this@MainActivity).getReferrer()
                deferredUri?.let { handleDeepLink(it.toUri()) }
            }
        }
    }
}

Unified routing

One function handles both direct and deferred links:

fun handleDeepLink(uri: Uri) {
    when (uri.pathSegments.firstOrNull()) {
        "shoes" -> navController.navigate("shoes/${uri.lastPathSegment}")
        "promo" -> navController.navigate("promo?code=${uri.getQueryParameter("code")}")
        else -> navController.navigate("home")
    }
}

Test both paths: click link with app installed (direct), and click link → install → open (deferred).

Attribution SDKs

For marketing campaigns requiring cross-platform attribution, SDKs like Branch, Adjust, or AppsFlyer handle:

Firebase Dynamic Links was deprecated; Google's recommended path is using App Links + Install Referrer directly, or a third-party attribution SDK for marketing use cases.

If you use an SDK, still implement App Links verification — the SDK relies on the same underlying mechanisms.

Testing deferred links

# Simulate install referrer via adb (requires debug build)
adb shell am broadcast -a com.android.vending.INSTALL_REFERRER \
  -n com.example.app/com.example.app.InstallReferrerReceiver \
  --es "referrer" "deep_link=https%3A%2F%2Fexample.com%2Fshoes%2Fsale"

Test matrix:

Common failures

Referrer read too late. If your splash screen navigates to home before the referrer callback returns, the user sees a flash of home then jumps — or never jumps. Show a loading state until referrer resolution completes on first launch.

Missing assetlinks.json. App Links verification fails silently. Check Play Console → App Links verification status.

Routing only in onCreate. Deep links from notifications and app shortcuts arrive via onNewIntent(). Handle both:

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    intent.data?.let { handleDeepLink(it) }
}

Common production mistakes

Teams get deeplink attribution install wrong in predictable ways:

Shipping deeplink attribution install on Android fails quietly when you test only on flagship devices, skip process-death scenarios, or assume minSdk behavior matches latest API docs. Emulator-only validation misses OEM-specific battery optimizations and background execution limits.

Debugging and triage workflow

When deeplink attribution install misbehaves in production, work top-down instead of guessing:

  1. Confirm scope — one tenant, region, or deployment stage? Narrow blast radius before deep diving.
  2. Check recent changes — deploys, flag flips, config pushes, and schema migrations in the last 24 hours.
  3. Compare golden signals — latency, error rate, saturation, and traffic for the affected surface vs. baseline.
  4. Reproduce minimally — smallest input or scenario that triggers the failure; capture traces/logs with correlation IDs.
  5. Fix forward or rollback — if rollback is faster than root-cause during incident, rollback first, postmortem second.
  6. Add a guard — alert, integration test, or circuit breaker so the same class of failure is caught earlier next time.

Document the timeline during triage. Future you (and on-call) will need timestamps, not just conclusions.

Resources

Frequently asked questions

What is a deferred deep link?

A deferred deep link routes a user to specific in-app content after they install the app, even though they clicked the link before installing. The user clicks an ad or shared link, goes to the Play Store, installs, opens the app for the first time, and lands on the intended screen — not the home screen.

How does Android pass install attribution data?

The Play Install Referrer API provides the referrer URL that led to the Play Store install. Combined with App Links verification and attribution SDKs (Firebase Dynamic Links successor patterns, Branch, Adjust), you can match the install to the original link click and extract routing parameters.

What is the difference between deep links and App Links?

Deep links use a custom URI scheme (myapp://path) that opens your app if installed. App Links use HTTPS URLs (https://example.com/path) with domain verification, opening your app directly without a disambiguation dialog. App Links are preferred for production — they're more reliable and work for deferred linking with proper attribution setup.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →