How to Track App Installs From a Link on iOS and Android
How to track app installs from a link on iOS and Android: the redirect, the first-launch match, what the confidence scores mean, and a seven step setup.
TL;DR: To track app installs from a link you need three things: a redirect you control, code inside the app that reports its first launch, and a server that joins the two. A plain store URL gives you none of them, because the App Store passes nothing through the install and the Play install referrer only helps if something set it and something in your app reads it back. On Android the join is exact: the redirect appends a referrer carrying the link id to the Play Store URL, the app reads it on first launch through the Play Install Referrer, and the match scores 1.0. On iOS there is no referrer, so the install is matched on the identifier for vendor when the same install asks again, and otherwise on a probabilistic fingerprint of the request address, the device language, and the IANA timezone, which reaches its 0.85 ceiling only for the strongest signal set on an unambiguous match within the first hour and scores lower on a shared connection or with more time elapsed. Every result carries a match type, a confidence score, and a guaranteed flag that is true only for an exact match, which is the field to gate sensitive routing on. No advertising identifier is involved at any point, so no tracking permission is requested. The rest of this guide is the mechanism per platform, then a seven step setup you can follow today.
How to Track App Installs From a Link: The Three Pieces
Tracking an install back to a link sounds like one feature. It is three separate pieces of infrastructure, and every failed attempt is missing one of them.
- A redirect you control. The URL you share must resolve on a server you own before it sends the visitor to the store. That moment is the only chance anyone has to record that a tap happened, what link it was, and what the request looked like: the address it came from, the browser's language, the timezone. Once the store takes over, the trail stops.
- Something in the app that reports first launch. An install is invisible from the outside. The only place that knows a fresh copy of your app just opened for the first time is the app itself, so the first launch has to report in and bring whatever identifying signals the platform allows.
- A join between the two. The click and the first launch are two events, on two devices as far as your servers can tell, minutes or hours apart. Attribution is the claim that they belong to the same person, plus an honest number saying how sure you are.
Miss the first and you have installs with no source. Miss the second and you have clicks with no outcome. Miss the third and you have two tables nobody can reconcile.
Be precise about what the finished system answers, because it is narrower than "where do my users come from". It answers: this install came from that link, the one in the newsletter, on the poster, or shared by that specific user. That covers organic, referral, creator, email, community, and printed traffic, which for most small teams is nearly all of it. It does not cover a paid install bought through an ad network, because the tap happened inside another company's app on a click URL you never see. Those campaigns are what an enterprise mobile measurement partner exists for.
The rest of this guide walks each piece: why the stores do not give you the first one for free, what the join looks like on Android and on iOS, how to read the confidence numbers that come back, and the setup, step by step.
Why a Plain Store Link Cannot Do It
The obvious approach is to share the store URL directly. It fails for a structural reason on each platform.
On iOS, nothing survives the App Store. A tap on an App Store URL opens the store, the user installs, and the app launches with no record of where it came from. There is no equivalent of a referrer that the store hands to the app. The link's destination has to be reconstructed after the fact, not delivered, which is why every iOS attribution answer is either a device-scoped identifier or a probabilistic guess.
On Android, the referrer exists but nobody sets it for you. The Play Store honors a referrer query parameter on a listing URL and stores its value against the install, then hands it back to the app afterwards. That is a genuine deterministic handoff, and it is why Android is the easy platform. But it only works if two things happen: the URL the visitor followed carried a referrer value that identifies the link, and your app reads that value back through the Play Install Referrer library on first launch. A bare play.google.com/store/apps/details?id=... link you paste into a post carries nothing, so there is nothing to read.
There is a second trap on Android. A market://details?id=... address is a custom scheme, not a web address, so a redirect that stops there never reaches a play.google.com URL and the referrer is never attached. Use the https://play.google.com form as your store URL, or you lose the one deterministic signal the platform gives you.
So the shared URL has to be a link on a domain you control, which records the click and then forwards to the store. Everything else in this guide depends on that single decision.
The Android Path: The Play Install Referrer
Android gets an exact answer, and the mechanism is worth understanding because it is the reference the iOS path is compared against.
At click time, the redirect resolves the link, records the click, and sends the visitor to the Play Store with a referrer appended:
https://play.google.com/store/apps/details
?id=com.yourcompany.yourapp
&referrer=utm_source%3Dwarplink%26utm_content%3D{link_id}
The utm_source value marks the referrer as one the SDK should claim, so an install that arrived from anywhere else is reported as the ordinary organic case rather than as a failure. The utm_content value is the payload: the link's identifier, which is what makes the match exact.
At install time, the app asks the Play Store app for that string through Google's Play Install Referrer library. Three properties of that read shape the design:
- It can hang. The library binds to a service inside the Play Store app, which may be updating, disabled, or absent. A first launch cannot wait on a service that will never answer, so the read runs on a 2 second budget and then falls through to the probabilistic path.
- It is worth calling once. Google's guidance is to read the referrer on the first execution after install, which is exactly when an attribution SDK runs its check.
- It carries a timestamp. The library also returns the moment the Play install began, which is how you tell a genuine new install from an app that has been sitting on the device for months. Attributing an old install is correct. Dropping a long-time user into content from four months ago is not, so routing is bounded to installs that began within the last 7 days while attribution is not bounded at all.
When the referrer names one of your links, the match is deterministic: match type deterministic, confidence 1.0, and the guaranteed flag set to true.
The referrer is missing in several ordinary situations, and all of them produce the same fallback rather than an error:
- the app was sideloaded with
adb installor handed over as a file, so it never went through the Play Store, - the device has no Google Play Services, so nothing can bind to the referrer service,
- the Play Store service refuses the bind or reports the feature as unsupported,
- the referrer data expired, or names a source that is not yours.
In each case the SDK falls back to fingerprint matching with no code change on your side. That fallback is the same one iOS uses for every first install, so it is worth understanding on its own.
The iOS Path: Vendor Identifier and Fingerprint
iOS has no referrer, so the join is built from what the device can say about itself at launch and what the browser said about itself at click time.
The identifier for vendor (IDFV) is the strongest signal available. It is a UUID shared by every app from the same vendor on one device, always available, and not gated by App Tracking Transparency. When it answers, the match is deterministic and scores 1.0. What it covers is narrower than it looks: it answers the same install asking again, where an earlier attribution record for the device already exists. A genuine first install has no prior record to match against, and a reinstall the SDK reports as one is deliberately left to the tier below so it is recorded as the new install it is. The value is also reset once the last app from a vendor leaves the device, so a single-app vendor sees a fresh one after a full delete anyway.
The fingerprint covers everything else. It compares three signals captured in the browser at click time against the same three collected in the app at launch time:
| Signal | Where it comes from | Note |
|---|---|---|
| Address of the request | Derived server side, on both the click and the launch | An app cannot see its own public address, so the device never sends one |
| Preferred language | The device locale, normalized to its primary subtag | Sent raw, for example en-US |
| Timezone | The IANA zone name where available, the minute offset otherwise | The zone name carries far more entropy than the offset |
The address is the part most teams get wrong when they build this themselves. The SDK inside your app has no reliable way to know the public address its packets exit from, so a hash computed on the device could never match one computed at click time. The fix is to have the device send raw signals and let the server compute both hashes from the addresses it sees on each request. It is also why iCloud Private Relay lowers confidence rather than breaking anything: a relayed exit address seen at one end and not the other keys into a different bucket, which weakens or drops a probabilistic match and leaves the exact tiers untouched.
Which signals the app can supply decides the fingerprint variant, and the variant sets how high a probabilistic match is allowed to score. enriched_tz hashes the IANA zone name and earns the highest band, enriched hashes the minute offset instead and scores a little below it, and basic drops the timezone entirely, keys on the address and the language alone, and scores lowest of the three.
What the Numbers Mean
Every match arrives with three fields, and following this workflow you need one rule for each. match_type says whether the answer was exact or a guess. match_confidence scores a guess: 0.85 is the ceiling for the strongest signal set on an unambiguous match within the first hour, a household IPv4 connection scores lower, around 0.77, a mobile carrier connection behind CGNAT lower still, around 0.51, and every score drops further with time and when several recent clicks share the same fingerprint. Route to specific content above 0.5 and show something softer below it. match_guaranteed is true only for an exact match, the Play Install Referrer or the vendor identifier, and it is the field to gate auto sign-in and personal data on, never a score.
Two counting rules matter when you compare reports: a click is unique per link, address, and 24 hour window, computed when the click is written, so one reader tapping four times counts once, and a reinstall is a real install that is attributed again from scratch and reported together with installs as one number. The rest of the scoring model, including the per-variant bands, the multipliers that only ever reduce a score, and the reasons a match comes back empty, is in Install Attribution Without an MMP.
The Setup, Step by Step
The mechanism above is the same wherever it runs. The steps below use the WarpLink SDKs, because the samples have to name something concrete, and the shape of the work is the same for any service that does link-based attribution.
Step 1: Register your app with both store URLs
Register the app once with the identifiers each platform uses: the iOS bundle identifier and team identifier, the Android package name and signing fingerprints. Then give it both store URLs, and make the Android one a https://play.google.com address. That single field is what decides whether your Android installs are exact or probabilistic, because the install referrer is only attached when the visitor is sent to a Play Store URL.
Step 2: Create one link per channel
Create a separate link for every place you share, from the dashboard or the API. Set the destination, the in-app route for each platform, and the campaign values that describe the channel:
curl -X POST https://api.warplink.app/v1/links \
-H "Authorization: Bearer wl_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination_url": "https://example.com/spring-sale",
"ios_url": "myapp://sale/spring",
"android_url": "myapp://sale/spring",
"app_id": "YOUR_APP_ID",
"parameters": {
"injected": [
{ "key": "utm_source", "value": "newsletter", "mode": "always" },
{ "key": "utm_campaign", "value": "spring-sale", "mode": "always" }
],
"inheritOrgInjected": true,
"suppressedOrgKeys": []
}
}'
myapp://sale/spring is an in-app route, so it only opens for someone who already has the app. Everyone else is sent to the link's fallback URL if it has one, and to the app's store URL if it does not, which is the path that produces the install you are about to attribute.
Step 3: Add the SDK and configure it with an SDK key
Add the SDK for each platform you ship and configure it as early as possible in the launch sequence. Use an SDK key, created in the dashboard under API Keys, not an ordinary API key. The two look identical, and an API key cannot carry the attribution scope, so an app configured with one keeps resolving deep links while every attribution call is rejected and no installs ever appear. It is the single most common way this integration looks healthy and reports nothing.
Step 4: Read the attribution result on first launch
The check runs once on the genuine first launch after an install, and the result arrives in the same callback that receives ordinary link taps. The deferred flag is what separates the two.
import SwiftUI
import WarpLink
@main
struct MyApp: App {
init() {
WarpLink.configure(
apiKey: "wl_live_yoursdkkeyhere000000000000000000",
options: WarpLinkOptions(onLink: { result in
guard case .success(let deepLink) = result, let deepLink else { return }
if deepLink.isDeferred {
recordInstall(deepLink)
}
route(deepLink)
})
)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
func recordInstall(_ deepLink: WarpLinkDeepLink) {
analytics.track("install_attributed", [
"link_id": deepLink.linkId,
"match_type": deepLink.matchType?.rawValue ?? "none",
"confidence": deepLink.matchConfidence ?? 0,
"guaranteed": deepLink.matchGuaranteed,
])
}
The Kotlin equivalent goes in Application.onCreate, where the referrer read and the first-launch check are already wired:
import android.app.Application
import app.warplink.WarpLink
import app.warplink.WarpLinkDeepLink
import app.warplink.WarpLinkOptions
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
WarpLink.configure(
context = this,
apiKey = "wl_live_yoursdkkeyhere000000000000000000",
options = WarpLinkOptions(onLink = { result ->
result.onSuccess { deepLink ->
if (deepLink.isDeferred) {
recordInstall(deepLink)
}
route(deepLink)
}
})
)
}
private fun recordInstall(deepLink: WarpLinkDeepLink) {
analytics.track(
"install_attributed",
mapOf(
"link_id" to deepLink.linkId,
"match_type" to deepLink.matchType?.name,
"confidence" to deepLink.matchConfidence,
"guaranteed" to deepLink.matchGuaranteed,
),
)
}
}
In React Native the attribution result is also available on its own, which suits an app that reports installs from one place rather than from the routing callback:
import { WarpLink, type AttributionResult } from '@warplink/react-native';
export async function reportInstallSource(): Promise<void> {
const attribution: AttributionResult | null =
await WarpLink.getAttributionResult();
if (!attribution) {
analytics.track('install_organic');
return;
}
analytics.track('install_attributed', {
linkId: attribution.linkId,
matchType: attribution.matchType,
matchConfidence: attribution.matchConfidence,
matchGuaranteed: attribution.matchGuaranteed,
isDeferred: attribution.isDeferred,
});
}
Step 5: Route on the match, not on the score
Reporting the install is one decision. Sending the user somewhere is another, and it deserves a stricter rule:
func route(_ deepLink: WarpLinkDeepLink) {
if deepLink.matchGuaranteed {
// Exact match. Safe for anything, including personal data.
restoreSession(for: deepLink)
return
}
let confidence = deepLink.matchConfidence ?? 0
if confidence > 0.5 {
navigateTo(deepLink.deepLinkUrl ?? deepLink.destination)
} else {
showWelcome(suggesting: deepLink.destination)
}
}
Above 0.5 the guess is good enough for public content. Below it, a welcome screen that names the content and lets the user opt in is a better experience than a hard redirect you are 30 percent sure about.
Step 6: Watch the installs land against the link
Clicks and attributed installs appear as they happen, per link and per campaign, with no hourly rollup and no overnight batch between the tap and the number. Break them down by device, country, referrer, or UTM values, and export the numbers as CSV when you want them in a spreadsheet or a warehouse. How far back you can look is set by your plan, from 90 days on the free plan up to three years.
This is also where a broken configuration shows itself. Clicks rising with no installs against a link whose Android destination is a web page means Android installs are falling back to the probabilistic tier, and the fix is the store URL from step 1.
Step 7: Verify with one real install per platform
Do the real thing once per platform before you trust the numbers:
- iOS. Delete the app, tap the link on the device, install from TestFlight, and open it. An Xcode rerun is not a fresh install and returns the cached first-launch result, which is the most common reason a working integration looks broken.
- Android. Uninstall, tap the link in Chrome, and install through Google Play, in practice the internal testing track. An APK pushed with
adb installnever carries a referrer, so it exercises the fingerprint path rather than the deterministic one.
Keep the gap between the tap and the install short, ideally inside an hour. The match window, which is how far back the server will look for a click to match an install against, defaults to six hours and is capped at 24, and a short gap also lands you in the high confidence band while you are checking that the numbers are right.
One Link Per Channel, and the Values That Ride Along
Attribution answers which link drove an install. That answer is only as useful as your links are specific.
Give every placement its own link: the newsletter, the profile bio, the poster in the shop window, the community post, the referral each user shares. One link shared everywhere collapses into a single row that says installs happened and nothing about where. Ten links cost nothing and turn the same installs into a channel report.
Campaign values are the second half of that. The standard utm_source, utm_medium, utm_campaign, utm_term, and utm_content fields are read off the URL the visitor tapped and stored on the click record the install is later matched against, so values you put on the URL you share group installs the same way they group everything else in your reporting. Values a link injects go somewhere else: they are appended to the destination at redirect time, so they tag the page and whatever analytics runs there rather than the click. Set them once on the link the way step 2 does, with mode always when the link's value must win and mode default when it should only fill a gap, and tag the URL you share as well when you want both.
One link per placement stops being hand work the moment links become per user. A referral feature needs a link per member and a share sheet needs one per shared item, which is the same POST /v1/links call from step 2 with your backend in front of it, filling the campaign values from whatever context created the link. The attribution result then names that specific link, so an install traces back not to "referrals" but to the person whose share produced it.
Where this pays off is the comparison a single blended number cannot make. A channel that brings a lot of clicks and few installs is a targeting problem. A channel that brings few clicks and many installs is where the next share should go. Neither reading exists if every share went through one link.
Privacy: No Advertising Identifier, No ATT Prompt
Nothing in this workflow touches an advertising identifier. There is no IDFA on iOS and no GAID on Android, and no App Tracking Transparency prompt, because the vendor identifier is scoped to your own apps and exempt from it while the Play Install Referrer needs no user permission at all. What remains is the request address, the device language, and the timezone, stored only as a one-way hash and sent over HTTPS. The full privacy reference, including the signals that are deliberately not collected, is on the install attribution concept page.
Frequently Asked Questions
Can I track app installs from a plain App Store or Play Store link? No, not per link. A store URL sends the visitor to a listing and nothing about which link they tapped survives the install on iOS, so there is nothing to attribute against. Android is closer, because the Play Store carries an install referrer through the install, but only if the URL you sent the visitor to carried a referrer parameter and only if your app reads it back on first launch. Both platforms need a redirect you control in front of the store URL.
Does the App Tracking Transparency prompt change my per-link install counts? No, and the prompt never appears for this. Per-link counts come from the Play Install Referrer, the vendor identifier, and a server-computed fingerprint, so a user who taps "Ask App Not to Track" is counted exactly like one who does not. What actually moves your per-link numbers is whether your Android destination is a play.google.com URL and how soon people install after tapping.
How accurate is install tracking on iOS compared with Android? Android is exact when the install came through Google Play, because the referrer names the link itself and the match scores 1.0. iOS has no referrer, so a genuine first install is matched probabilistically on the request address, the device language, and the timezone. 0.85 is the ceiling for the strongest signal set on an unambiguous match within the first hour. A household IPv4 connection scores lower, around 0.77, and a mobile carrier connection behind CGNAT lower still, around 0.51, and every score drops further with time and when several recent clicks share the same fingerprint. The full best-case and weakest-variant table is in Install Attribution Without an IDFA. Read the score on every result and gate anything sensitive on the guaranteed flag rather than on a threshold.
Does install tracking work for QR codes and email links? Yes, because both are just taps on a URL. A camera scan opens the URL in the default browser, and a tap in an email opens it either in the default browser or in the mail client's own in-app browser, so in each case the redirect records a click before the visitor reaches the store. Give every placement its own link so the scan on a poster and the tap in a newsletter are separate rows rather than one blended number.
Is install tracking from a link real time? Clicks are recorded as they happen and an install is attributed on the app's first launch, which is usually seconds after it opens. There is no overnight batch and no hourly rollup between the tap and the number you read. The gap you do see is human: the minutes or hours a person spends downloading the app before opening it.
Can I track installs from a link without adding an SDK to my app? No. A redirect can count the click, but only code inside the app can report that a first launch happened, and without that report there is no install to join the click to. On Android you would have to read the Play Install Referrer yourself, and on iOS you would have to collect and send the device signals a match needs. That code is what an attribution SDK is.
Related Guides
- The concepts behind this workflow: Install Attribution Without an MMP: A Guide for Small Mobile Teams covers the match cascade as a model, the boundary with paid user acquisition, and when a measurement partner is the right call.
- The iOS mechanism in depth: Deferred Deep Linking on iOS: How to Implement It in Swift walks the vendor identifier and fingerprint tiers, and the Swift implementation.
- The Android mechanism in depth: Deferred Deep Linking on Android: How to Implement It in Kotlin covers the Play Install Referrer end to end, including what to do when it is missing.
- Tracking scans: QR Code Deep Linking: Route Scans Into Your App, Even Before Install applies the same setup to printed and on-screen codes, one link per placement.
- Reference: how attribution works documents every tier, every score, and what is collected, while the install attribution page is the one-screen summary.
How WarpLink Helps
The two halves you cannot host inside your app are the click recorder that runs before the store takes over and the endpoint that computes the fingerprint from the request and walks the match cascade for you. That is WarpLink's job. In your app the whole thing collapses to one configure call and one result carrying the link id, the match type, the confidence, and the guaranteed flag, on iOS, Android, and React Native.
One workflow, three pillars. Deep linking puts the new user on the screen the link promised, install attribution credits the link and channel that produced them, and real-time analytics turns a week of that into a channel report you can act on. Sub-10ms redirects on the way in, one match on the way out, three answers from the same SDK.
Create a free WarpLink account and get deep linking, install attribution, and real-time analytics in one SDK, with 10,000 clicks a month on the free plan and no time limit. The attribution concept page is the reference behind every number above.
WarpLink Team
Building affordable, reliable link infrastructure for mobile teams. Deep linking, install attribution, and real-time analytics in one SDK.
Related Posts
QR Code Deep Linking: Route Scans Into Your App, Even Before Install
A QR code deep link is an HTTPS URL that a scan opens with a real user tap, so it must point at a redirect you control. How to route and attribute every scan.
Install Attribution Without an MMP: A Guide for Small Mobile Teams
Install attribution connects a new install back to the click that caused it. How to attribute link-driven installs on iOS and Android without an MMP.
Deferred Deep Linking on Android: How to Implement It in Kotlin
Deferred deep linking on Android hands the tapped link to the app on first launch through the Play Install Referrer, with fingerprint matching as the fallback.