Deep Linking — Appsflyer

Deep Linking — From Zero to Senior Level
Let me teach you this like you've never heard these words before. We'll build up slowly.
LEVEL 1 — Before We Even Talk About Deep Links
First, understand what a "link" does on web
You're on Google. You click this link:
https://amazon.in/shoes/nike-air-max
Your browser opens Amazon. And it goes directly to that Nike shoe page.
That's just a normal web link. Simple.
Now the question is —
Can the same thing happen inside a mobile app?
Can I click a link and land directly on a specific screen inside Amazon app — not the home screen?
Yes. That's exactly what Deep Linking is.
What happens WITHOUT deep linking
User clicks link
→ Amazon app opens
→ Home screen shows
→ User has to search manually
→ User gets frustrated
→ User leaves
What happens WITH deep linking
User clicks link
→ Amazon app opens
→ DIRECTLY on Nike shoe page
→ User sees what they wanted
→ User buys
Same app. Completely different experience.
LEVEL 2 — How Does Android Even Open an App From a Link?
This is where we get technical. But stay with me — it's simpler than it sounds.
Everything in Android is an Intent
In Android, when something happens — a button tap, a notification tap, a link click — Android wraps that event in something called an Intent.
Think of Intent as an envelope.
┌─────────────────────────────┐
│ INTENT │
│ │
│ Action: VIEW │
│ Data: https://amazon.in/ │
│ shoes/nike-123 │
└─────────────────────────────┘
Android delivers this envelope to your app.
Your app opens it, reads the URL, and decides — "okay, show the shoe screen."
That's the entire magic. Literally.
How does Android know WHICH app to send the Intent to?
You declare it in your AndroidManifest.xml.
You're basically telling Android:
"Hey, if anyone sends an Intent with this URL pattern — give it to me."
<activity android:name=".MainActivity">
<intent-filter>
<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="amazon.in"
android:pathPrefix="/shoes" />
</intent-filter>
</activity>
Reading this simply:
scheme="https"→ the URL starts withhttps://host="amazon.in"→ the domain is amazon.inpathPrefix="/shoes"→ URL contains /shoes
When all three match → Android sends the Intent to this Activity.
What does your Activity do with it?
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Open the envelope
val intent = intent
val data: Uri? = intent.data
if (data != null) {
// Read the URL
val path = data.path // "/shoes/nike-123"
// Navigate to correct screen
if (path?.startsWith("/shoes") == true) {
openShoeScreen(path)
}
}
}
}
That's it at the basic level. You read the URL. You navigate. Done.
LEVEL 3 — The 3 Types of Deep Links
Now you understand the concept. Let's talk about the 3 ways to actually implement it. Each one is an evolution of the previous.
Type 1 — Custom URI Scheme (The Old Way)
Instead of https://, you invent your own scheme.
amazon://product/nike-123
This is like a private road only your app knows about.
Manifest declaration:
<data android:scheme="amazon" />
Why it was used: Simple. Easy. No setup needed.
Why you should NOT use it today:
Problem 1 — No web fallback
If the app isn't installed and someone clicks amazon://product/123 in a browser — the browser has no idea what to do. It just shows an error. Dead end.
Problem 2 — App chooser popup
If two apps both declare amazon:// — Android shows a popup asking the user "which app do you want to open this with?" Terrible experience.
Problem 3 — Security
Any app on the device can declare amazon:// and steal your deep links. There's no ownership verification.
Type 2 — Android App Links (The Right Way)
Instead of a custom scheme, you use a real HTTPS URL.
https://amazon.in/product/nike-123
This is a normal web URL. But when Amazon app is installed — it opens in the app, not the browser. No popup. Directly.
How does Android trust that Amazon owns amazon.in?
This is the key question. The answer is called Digital Asset Links.
Amazon hosts a JSON file on their server:
https://amazon.in/.well-known/assetlinks.json
That file says:
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.amazon.mShop.android.shopping",
"sha256_cert_fingerprints": ["AB:CD:EF:..."]
}
}]
Reading this simply:
"The app with this package name and this certificate fingerprint is allowed to handle URLs from amazon.in"
Android checks this file when your app is installed. If it matches — your app is verified as the owner of that domain.
Result:
No app chooser popup
Opens directly in app
Secure — no other app can steal it
This is what all major apps use today.
Type 3 — Deferred Deep Link (The Advanced Marketing Case)
Now here's the tricky one. Pay attention.
The problem it solves:
User sees Instagram ad for Nike shoes
User clicks the ad link
User does NOT have Amazon app installed
User is sent to Play Store
User installs Amazon
Amazon opens
→ WHERE does the user land?
Without deferred deep linking → Home screen. The context is lost.
With deferred deep linking → Directly on Nike shoes page. Context preserved.
But how? The app wasn't even installed when the link was clicked.
This is genuinely hard. Android has no built-in solution. This is where AppsFlyer comes in. We'll explain the full mechanism in Level 5.
LEVEL 4 — Handling Deep Links Properly in Code
Now let's get into real implementation problems that actually trip people up.
The Cold Start vs Warm Start Problem
This is one of the most common bugs in production.
Cold Start = App is not running at all. User clicks link. App launches fresh.
In this case, the deep link arrives via onCreate():
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleDeepLink(intent) // intent has the link data
}
Warm Start = App is already running in the background. User clicks link. App comes to foreground.
In this case, onCreate() is NOT called again. The deep link arrives via onNewIntent():
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleDeepLink(intent) // must handle here too
}
If you only handle onCreate() and forget onNewIntent() — warm start deep links silently fail.
The fix is simple — handle both:
private fun handleDeepLink(intent: Intent) {
val data: Uri? = intent.data
if (data != null) {
navigateBasedOnUri(data)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleDeepLink(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent) // important — update the activity's intent
handleDeepLink(intent)
}
Deep Linking with Jetpack Navigation Component
Modern apps use Jetpack Navigation. Here's how deep links integrate.
You declare deep links directly in your nav graph XML:
<fragment
android:id="@+id/shoeDetailFragment"
android:name=".ShoeDetailFragment">
<deepLink
android:id="@+id/deepLink"
app:uri="https://amazon.in/shoes/{shoeId}" />
</fragment>
Now when https://amazon.in/shoes/nike-123 arrives — Navigation Component automatically routes to ShoeDetailFragment and passes shoeId = "nike-123" as an argument.
You receive it in the Fragment like any other argument:
val shoeId = arguments?.getString("shoeId")
The important rule: Handle the Intent at the Activity level and pass it to your NavController. Do not try to handle deep links inside individual Fragments — this creates back stack issues and breaks on configuration changes.
// In MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navController = findNavController(R.id.nav_host_fragment)
navController.handleDeepLink(intent) // let NavController handle it
}
Deep Linking in Jetpack Compose
Compose uses a slightly different approach with NavHost:
NavHost(navController = navController, startDestination = "home") {
composable(
route = "shoes/{shoeId}",
deepLinks = listOf(
navDeepLink {
uriPattern = "https://amazon.in/shoes/{shoeId}"
}
)
) { backStackEntry ->
val shoeId = backStackEntry.arguments?.getString("shoeId")
ShoeDetailScreen(shoeId = shoeId)
}
}
Critical rule in Compose: Your NavController must be created at the top level and passed down. Never create it inside a Composable that might recompose — deep link handling will break.
LEVEL 5 — How AppsFlyer and Deferred Deep Linking Works Internally
Now we go deep. This is senior-level territory.
First — What is the Install Referrer API?
Google Play has a built-in mechanism called the Install Referrer API.
Here's what it does:
When someone clicks a link with campaign data and installs your app through Play Store — Play Store preserves the referrer string through the install process.
Example link:
https://play.google.com/store/apps/details
?id=com.amazon.app
&referrer=campaign%3Dramadan_shoes%26product%3Dnikeair
After install, your app can query Play Store:
val referrerClient = InstallReferrerClient.newBuilder(context).build()
referrerClient.startConnection(object : InstallReferrerStateListener {
override fun onInstallReferrerSetupFinished(responseCode: Int) {
val response = referrerClient.installReferrer
val referrerUrl = response.installReferrer
// "campaign=ramadan_shoes&product=nikeair"
// Now you know where this install came from
}
})
AppsFlyer uses this internally to recover attribution data on first launch.
The Full Deferred Deep Link Flow
Let's walk through it step by step with a real scenario.
Step 1 — User clicks the ad
The ad has an AppsFlyer link:
https://app.appsflyer.com/com.amazon.app
?pid=facebook
&c=ramadan_campaign
&deep_link_value=shoes/nike-123
Step 2 — AppsFlyer server logs the click
AppsFlyer records:
Click received
Time: 2:34 PM
Campaign: ramadan_campaign
Source: facebook
Deep link value: shoes/nike-123
Device fingerprint: {IP, device model, OS version, screen size}
Step 3 — User goes to Play Store and installs
Play Store preserves the referrer. App gets installed.
Step 4 — First launch of the app
AppsFlyer SDK inside your app wakes up and contacts AppsFlyer servers:
"Hey, this app just launched for the first time.
Here's my device fingerprint."
Step 5 — Server matches install to click
AppsFlyer compares:
Install fingerprint vs stored click fingerprints
Referrer string from Play Store
If they match → Attribution confirmed.
Server responds:
{
"campaign": "ramadan_campaign",
"source": "facebook",
"deep_link_value": "shoes/nike-123",
"status": "attributed"
}
Step 6 — Your app receives the callback
AppsFlyerLib.getInstance().init(AF_DEV_KEY,
object : AppsFlyerConversionListener {
override fun onConversionDataSuccess(data: Map<String, Any>) {
val deepLinkValue = data["deep_link_value"] as? String
// "shoes/nike-123"
if (deepLinkValue != null) {
navigateTo(deepLinkValue) // open shoe screen
}
}
override fun onConversionDataFail(error: String) {
// attribution failed, open home screen
}
},
this
)
User lands on the Nike shoe page — even though the app wasn't installed when they clicked the link.
What if Install Referrer Fails? — Probabilistic Matching
Sometimes the referrer data is lost (privacy settings, certain devices). AppsFlyer falls back to probabilistic matching.
It compares the device fingerprint from the click event with the fingerprint from the install event:
Click fingerprint: IP=103.x.x.x, Device=Samsung S23, OS=Android 14, Time=2:34 PM
Install fingerprint: IP=103.x.x.x, Device=Samsung S23, OS=Android 14, Time=2:41 PM
Same IP, same device, 7 minutes apart → High probability this is the same user → Attribution confirmed.
Not 100% accurate, but works well enough for marketing purposes.
LEVEL 6 — Security (What Most Devs Ignore)
Deep links are an attack surface. Here's what can go wrong.
Parameter Tampering
Someone sends a user this link:
myapp://payment/confirm?amount=1&discount=99999
If your app blindly processes these parameters — you have a serious bug.
Rule: Never trust deep link parameters for financial or security-sensitive operations. Always validate server-side.
// WRONG
val amount = uri.getQueryParameter("amount")
processPayment(amount) // dangerous
// RIGHT
val orderId = uri.getQueryParameter("orderId")
// fetch order details from YOUR server using orderId
// server validates everything
// then process payment
Intent Hijacking (Custom Scheme Problem)
Any app can declare myapp:// if you use custom schemes. A malicious app can intercept your deep links.
Android App Links with domain verification completely prevents this — because only the verified app gets those URLs.
Expired Links
Flash sale ended 2 hours ago but the link is still circulating on WhatsApp.
Always validate time-sensitive links server-side:
val campaignId = uri.getQueryParameter("campaignId")
// Call your API
api.validateCampaign(campaignId) { result ->
if (result.isActive) {
navigateToOffer()
} else {
showExpiredScreen()
}
}
LEVEL 7 — Interview Answer Framework
When an interviewer asks about deep linking, here's the mental structure to follow:
Start with types — explain the difference between custom scheme, App Links, and deferred deep linking. Mention why App Links are preferred (domain verification, no app chooser, security).
Go to implementation — manifest declaration, Intent handling, cold vs warm start, NavController integration.
Add the hard problem — deferred deep linking, how it requires an SDK, how Install Referrer and probabilistic matching work internally.
Close with production concerns — security validation, expired links, back stack management.
That progression shows you've actually built and maintained this in production — not just read about it.
Comments (0)
No comments yet. Be the first to share your thoughts!
Leave a Comment
Get the latest updates
Join our newsletter and never miss an article.