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 match key 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. 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 route on. Keep sign-in and personal data on your own authentication. 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 a confidence score 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.
The finished system answers which link produced each install: 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.
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, the match happens after the install. A tap on an App Store URL opens the store and the user installs. When the app launches, the link's destination is reconstructed from the tap rather than delivered by the store.
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 runs on a budget. The read uses a 2 second budget and then continues to the probabilistic path, so first launch never waits.
- 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 SDK moves to probabilistic matching on its own whenever the Play Store has no referrer to give, with no code change on your side. The cases are:
- 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.
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 Probabilistic Match
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 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, it returns the install the device already has, with that install's own match type and score, so it never turns a probabilistic match into a guaranteed one. It answers the same install asking again, where an earlier attribution record for the device 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 probabilistic match 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. The exact Play Install Referrer tier on Android is unaffected by relayed addresses such as iCloud Private Relay.
Which signals the app can supply decides the match key 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.
Putting the Android and iOS halves side by side, the order the server tries them in is the Play Install Referrer, then the IDFV for a device already seen, then the probabilistic match within its window, then raw signals hashed with the request IP. How to Attribute Installs Without a Click ID lays out that order and what each method can and cannot prove. See install attribution for the product view.
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. match_confidence scores a probabilistic match: 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. Route to specific content above 0.5 and show something softer below it. match_guaranteed is true only for an exact match, which for an install means the Play Install Referrer on Android, so an install matched on iOS reads false. Use it, not a score, to pick a destination, and keep sign-in and personal data on your own authentication.
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 Keys > SDK keys, not an ordinary API key. The two look identical, and an API key cannot carry the attribution scope.
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()
// Hands each tapped link to the SDK, which resolves it into onLink.
.onOpenURL { url in WarpLink.open(url) }
}
}
}
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, so route toward sign-in. Sign-in itself stays on
// your own authentication.
navigateToSignIn(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, route to 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 on a low-confidence score.
Step 6: Watch the installs land against the link
Clicks and attributed installs show in the dashboard within minutes, per link and per campaign. 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 the last 30 days on the free plan (90 days are retained) up to three years.
Point the link's Android destination at the Play Store URL from step 1 so the Play Install Referrer match applies.
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.
- 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 probabilistic 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.
Referral Campaign Attribution
A referral program is the same three pieces as any other channel, applied per referrer instead of per marketing placement. Each participant gets their own link rather than sharing one program-wide URL, so a tap traces back to the specific person who sent it, not just to "referrals" as a bucket.
Create the link the same way a share feature would: one POST /v1/links call per participant, with the destination, the in-app route, and a campaign value such as utm_content set to the referrer's ID or handle. Because attribution reads the match back to that specific link, WarpLink records who was referred, when the tap happened, and whether the install that followed used the deterministic Play Install Referrer or the probabilistic path on iOS, scored with the same confidence and guaranteed flag described earlier in this guide.
What that gives you is a per-referrer table in WarpLink's analytics: clicks, unique clicks, and attributed installs, filterable by link or by tag if you group referrers under one campaign. WarpLink reports which link produced an install. Build payout logic on top of that attribution result, reading the link ID and the referrer it maps to from your own records.
On Android the referrer carries through the Play Store with no time window, and on iOS the match window runs up to 24 hours.
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 match key, 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. The full best-case and weakest-variant table is in Install Attribution Without an IDFA. Read the score on every result and use the guaranteed flag, not a threshold, to pick a destination, and keep sign-in and personal data on your own authentication.
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 at tap time and an install is attributed on the app's first launch, which is usually seconds after it opens. Recent taps show in the dashboard within minutes. 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 iOS mechanism in depth: Deferred Deep Linking on iOS: How to Implement It in Swift walks the vendor identifier and probabilistic 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.
- Choosing an option: 6 Install Attribution Options for Small Apps compares store reports, a DIY referrer, and link-based services by what each one measures.
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 match key 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, React Native, and Flutter.
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. See how we measure this.
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.
See which link drove each install
Deep linking, install attribution, and real-time analytics in one SDK. Free for 10K clicks a month, no sales call.
Stuck on setup? Email support@warplink.app and we will help you get one real link working.
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 in Flutter: One Callback for iOS and Android
Deferred deep linking Flutter guide: route the first open after install with one WarpLink 1.1.1 callback, plus first-open test steps and confidence scoring.