
Android Install Referrer: Affiliate Attribution Guide
The Android Install Referrer is the only deterministic affiliate attribution a mobile app store gives away. You put a string on a Play Store link, Play keeps it through the install, and your app reads it back on first launch. Nothing is inferred, no advertising id is involved, and the answer is either the exact string you set or nothing. The App Store forwards nothing into a fresh install, which is why our iOS path falls back to a deferred server side match.
That is the good news and it is one paragraph long. The rest costs engineering time: a bound service with a lifecycle, a value living inside the Play Store app, install paths that return nothing, and two Google pages that disagree. Every Google fact below was checked on 2026-09-14.
#What Play actually forwards into the install
The install referrer is one string Google Play stores against an install and hands back to that app on request. You set it with the referrer query parameter on a Play Store URL, and Google's rule for that parameter is exact: "The referrer string must be URL encoded, 512 characters or less, and included in the referrer query parameter of the Google Play URL", from the user acquisition guide, checked on 2026-09-14.
The shape trips people up because there are two layers of encoding. Inside the referrer value you write an ordinary query string, then that whole string is percent encoded again so it survives as one parameter on the outer URL. Google's example makes the nesting obvious:
https://play.google.com/store/apps/details?id=com.sample.package&referrer=utm_source%3Dsearch%26utm_medium%3Dcpc%26utm_campaign%3DsummerpromoThe app receives utm_source=search&utm_medium=cpc&utm_campaign=summerpromo, decoded once, as a flat string. Nothing parses it for you and no field is reserved. Every affiliate link we generate for Play has the same shape, with one parameter:
https://play.google.com/store/apps/details?id=com.example.app&referrer=claim_token%3D0f9c4a1e5b2dOur links worker packs the claim token into referrer only when the destination host is play.google.com, and merges into a referrer the customer already set rather than replacing it: plenty of apps hand us a store URL carrying utm parameters for their own analytics, and deleting those breaks a report somebody else owns. Three tests in apps/links pin that behaviour.
We pack one parameter and not five because 512 characters is less room than it sounds. Every = becomes %3D and every & becomes %26, so four utm parameters and a signed token grow by roughly a third before reaching the wire. Creator, campaign and click resolve on our server instead, where no length limit shapes the design.
#Connect, read and disconnect without leaking the service
InstallReferrerClient binds to the Play Store app over AIDL. You build a client, start an asynchronous connection, wait for the setup callback, read the details inside it, and then release the binding. Google's reference is blunt about that last step: endConnection() should be called "once you are done with this InstallReferrerClient reference".
- Add
com.android.installreferrer:installreferrer:2.2. That version is the newest on Google's release notes, dated 2021-01-14 and unchanged when checked on 2026-09-14. - Build the client on a launch where you have not yet stored a referrer, not on every launch.
- Start the connection and branch on the response code, with a default branch for codes you do not recognise.
- Read
getInstallReferrer()inside theOKbranch. It throwsRemoteExceptionand works offline: Google notes it "uses information stored by the Google Play Store app without initiating a network request". - Persist the raw string before you parse it or send it anywhere, then call
endConnection(). - Reconnect from
onInstallReferrerServiceDisconnectedon your next request.
This is Google's connection sample, copied from the library page on 2026-09-14:
private lateinit var referrerClient: InstallReferrerClient
referrerClient = InstallReferrerClient.newBuilder(this).build()
referrerClient.startConnection(object : InstallReferrerStateListener {
override fun onInstallReferrerSetupFinished(responseCode: Int) {
when (responseCode) {
InstallReferrerResponse.OK -> {
// Connection established.
}
InstallReferrerResponse.FEATURE_NOT_SUPPORTED -> {
// API not available on the current Play Store app.
}
InstallReferrerResponse.SERVICE_UNAVAILABLE -> {
// Connection couldn't be established.
}
}
}
override fun onInstallReferrerServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
})The sample leaves out two of the five documented codes, and endConnection(). It is a skeleton, not an implementation.
| Constant | Value | Google's description | What we do with it |
|---|---|---|---|
| `OK` | 0 | "Success." | Read the details, persist, disconnect |
| `SERVICE_UNAVAILABLE` | 1 | "Could not initiate connection to the Install Referrer service." | Store nothing and try again next launch |
| `FEATURE_NOT_SUPPORTED` | 2 | "Install Referrer API not supported by the installed Play Store app." | Stop asking on this device, fall back |
| `DEVELOPER_ERROR` | 3 | "General errors caused by incorrect usage" | A bug in your code. Log loudly in debug builds |
| `SERVICE_DISCONNECTED` | -1 | "Play Store service is not connected now" | Transient. Reconnect, do not give up |
Values and descriptions copied from the InstallReferrerResponse reference on 2026-09-14.
Two Google pages contradict each other here, which matters before you write an exhaustive when. The release notes for version 2.2 say it "Added a new InstallReferrerResponse constant: PERMISSION_ERROR, which is returned whenever the app is not allowed to bind to the Service." The reference page's constants list carries five and PERMISSION_ERROR is not one of them. Both were open on 2026-09-14. Give the unknown code somewhere to land.
Reading the details, again from Google's library page:
val response: ReferrerDetails = referrerClient.installReferrer
val referrerUrl: String = response.installReferrer
val referrerClickTime: Long = response.referrerClickTimestampSeconds
val appInstallTime: Long = response.installBeginTimestampSeconds
val instantExperienceLaunched: Boolean = response.googlePlayInstantParamreferrerUrl is the flat query string described above, and with our SDK that string is the whole handoff:
MyAppAffiliate.applyInstallReferrer(response.installReferrer)It returns immediately, runs on the SDK's single background thread, and fails silently if the network is down, which on a first launch is common enough to design for.
#What ReferrerDetails carries, and which Google page to believe
Seven values, if you read the AIDL reference. Four, if you read the ReferrerDetails class reference, which documents getInstallReferrer(), getReferrerClickTimestampSeconds(), getInstallBeginTimestampSeconds() and getGooglePlayInstantParam() and stops there. Google's overview page sides with seven, listing the server side timestamps and the install version among what the API retrieves. Read the AIDL reference for the full set.
| Bundle key | Type | Google's description | Use in affiliate attribution |
|---|---|---|---|
| `install_referrer` | String | "The referrer URL of the installed package." | The attribution payload itself |
| `referrer_click_timestamp_seconds` | long | Client side timestamp when the referrer click happened | Device clock. Diagnostics only |
| `install_begin_timestamp_seconds` | long | Client side timestamp when installation began | Device clock. Diagnostics only |
| `referrer_click_timestamp_server_seconds` | long | Server side timestamp when the referrer click happened | The trustworthy click time |
| `install_begin_timestamp_server_seconds` | long | Server side timestamp when installation began | The trustworthy install time |
| `install_version` | String | "The app's version at the time when the app was first installed." | Separates a first install from a reinstall |
| `google_play_instant` | boolean | Whether the instant experience launched in the past 7 days | Instant apps only |
Field names and descriptions from Google's AIDL reference, checked on 2026-09-14.
The server side pair exists for a reason. Version 2.0 "Added new fields to the response of the getInstallReferrer() method to help developers understand and discover information about fraudulent clicks", per the release notes entry dated 2020-07-06. Client timestamps come from a clock the device owner can set to anything. For anything that decides who gets paid, use the server pair.
The gap between the two server timestamps is a click to install window measured by Google rather than by you, which is rare in attribution. An install beginning eleven days after the click is a fact, not an estimate. Same decision as how attribution windows work, better inputs.
We read none of the timestamps in the Android SDK today, because our own click record already carries a timestamp we generated. Building your own pipeline instead, the server timestamps are the honest input and cost nothing extra to read.
#Ninety days, one read, and which of those is actually true
The referrer does not evaporate when you read it. Google's caution says the information "will be available for 90 days and won't change unless the application is reinstalled", followed by advice: "To avoid unnecessary API calls in your app, you should invoke the API only once during the first execution after install." The second sentence is guidance about wasted work. It is not an expiry.
That distinction decides how you recover from a bad first launch. An app that read the referrer offline and dropped the string can read it again next launch, still inside the window. What you cannot do is wait out the 90 days or survive a reinstall, which replaces the stored value with whatever the new install carried.
An app update changes nothing. A value that "won't change unless the application is reinstalled" means an app shipping this integration for the first time today has no referrer for anybody who installed before it. Backfilling install attribution is not on the table.
Our own handling assumes the same. applyInstallReferrer hands the string to a background executor and returns; if the post to our API fails, the extracted token is persisted under a pending key and retried on a later launch. The comment in Client.kt gives the reasoning: a first launch is exactly when a device is most likely to be offline.
#Which install paths come back empty
Google is explicit that getInstallReferrer() "uses information stored by the Google Play Store app without initiating a network request". Coverage follows from that sentence. No Play install record means no stored information, so no referrer, and no error to separate that from an install nobody referred.
| Install path | Referrer available | What to do instead |
|---|---|---|
| Play Store, from a link carrying `referrer` | Yes, your exact string | Nothing. This is the deterministic case |
| Play Store, plain listing, search or a shared store link | Nothing Google documents | Log whatever arrives, attribute none of it |
| Sideloaded APK, `adb install`, file manager | No | Creator code, or an App Link after first open |
| Pre-installed on an OEM system image | No | Creator code |
| Samsung Galaxy Store, Amazon Appstore, AppGallery | No | Creator code, or a store specific referrer scheme |
| Play Store app older than the API | `FEATURE_NOT_SUPPORTED` | Server side deferred match |
| App not allowed to bind to the service | `PERMISSION_ERROR` per the 2.2 release notes | Handle the default branch, fall back |
The costliest failure is none of those, because all of those are visible. It is a referrer holding the wrong thing. Play validates that parameter's length and encoding and nothing about its meaning, so a creator who retypes your store URL, a newsletter tool that drops query parameters, or a shortener that keeps only the package id each produce a clean Play install with an unusable referrer. No exception, no response code, and you find out at payout.
Two defences are worth the effort: emit the link from a service you control rather than documenting a URL format and hoping, and log every referrer you could not parse, so a broken pattern shows up as a spike rather than as silence.
App Links answer a different question. Where the app is already installed, an App Link opens it with the query string intact and the install referrer is not involved, because nothing was installed. That path is MyAppAffiliate.attribute(intent). Running both is how one engine covers mobile and web, and the line between an affiliate platform and a full MMP.
#Is reading the install referrer tracking under Play's policy?
Play's Data safety documentation does not define "tracking" at all. It defines collection as "transmitting data from your app off a user's device" and sharing as "transferring user data collected from your app to a third party", and both cover libraries and SDKs your app includes. Sending a referrer string to an attribution backend is collection and sharing. Declare it.
The advertising id is a separate policy. Play requires apps to "use the advertising ID (when available on a device) in place of any other device identifiers for any advertising purposes", and apps targeting Android 13 or above must declare com.google.android.gms.permission.AD_ID, enforced across all devices since 1 April 2022. Persistent identifiers stay available for non advertising purposes such as analytics and fraud prevention, with a privacy policy in place. Quotes from Play's advertising ID policy, read on 2026-09-14.
The install referrer triggers none of that. The string is stored against the install, so an app reads a referrer with no advertising id, no AD_ID permission and no Play Services dependency. Our SDK generates a random UUID, persists it in SharedPreferences, and uses no advertising id and no fingerprinting, which keeps AD_ID out of your manifest. It does not keep you out of the Data safety form: Google defines "Device or other IDs" as identifiers that "relate to an individual device, browser or app", which a random install id plainly is.
We would rather declare an identifier we generated than an advertising id we did not need, and the cost is real: a user who clears app data looks like a new install to us. An SDK that quietly re-identified that device would be more accurate and less honest.
#Where MyAppAffiliate fits, and why we do not bundle the Play library
We do not bundle com.android.installreferrer. Our Android SDK is a plain Kotlin library with no Play Services dependency and minSdk 24, and the Install Referrer step is optional: add the Gradle dependency, run the lifecycle above yourself, hand us the string.
MyAppAffiliate.applyInstallReferrer(referrerString)Bundling would make the integration shorter. It would also drag a Google library, its AIDL service binding and its version upgrades into every host app that installs us, including apps shipping where Play is not the store and apps already pinned to another version. We took the second cost, so an Android developer here does more work than an iOS developer. That is a choice, not an oversight, and the full surface is in our Android SDK documentation.
Client.referrerHint parses the referrer with the same query string reader that handles a link, returning at most one of a claim token (claim_token, ct) or a referral code (via, ref, maa_code, code). The reuse is correct rather than lazy: a referrer is ordinary query string encoding, so the parser that reads ?via=LUMI off an App Link reads via=LUMI out of a referrer with no special case. Skip the Install Referrer and a Play install is still attributable, because start asks our API once per install for a deferred match on a hashed IP and a short window, which is probabilistic where the referrer is deterministic. Once per install counts answers, not attempts. A 404 or a 409 is an answer and closes the question for good, but a first launch with no network has been told nothing about this install, so the ask survives to the next launch and gives up only after three tries. First launch is exactly when a phone is most likely to be offline, which is why that distinction is worth a second stored key.
One gap to disclose, corrected in our own plan document on 2026-09-14. React Native is the single mobile SDK of ours with no first launch deferred call. Android, iOS and Flutter each run the once per install flow behind a maa.deferredTried flag; React Native posts only when a link or a code shows up. The referrer path still works there, but the fallback behind it is not built yet.
Commission rates, Google's cut, hold periods and creator payouts belong to the sibling post on running an affiliate program for an Android app. After the purchase, attribution joins on your user id rather than on the referrer, the same join RevenueCat attribution uses and the one the iOS setup reaches by another route. We charge a flat monthly fee and no revenue share.
#Test the referrer on a real device before you ship it
Testing this takes an afternoon and it is the only way to know your string survives the trip. An emulator with no Play Store, or an adb install of your release build, tells you nothing: neither writes the Play install record the API reads.
- Build the link and verify the encoding with a URL parser rather than by eye. Decode the
referrervalue once and confirm you get a flat query string. - Upload the build to an internal testing track and install it from the Play Store app on a physical device, starting from your own link.
- Log the raw string from
getInstallReferrer()before parsing it, and log the response code on every branch including the default. - Assert on the exact string you set. A test that passes because "a referrer arrived" also passes on an organic install.
- Uninstall, reinstall from a second link with a different token, and confirm the new value replaced the old one.
- Install the same build with
adb installon purpose and confirm your app records an unattributed install and moves on, instead of blocking first launch on a service that will never answer.
Do step 6. An attribution path that fails loudly on the phones of users who never touched Play is worse than no attribution.
Does the Install Referrer work for organic Play Store installs?
Google does not document what Play stores when no campaign set a value, so a string arriving on an organic install is not something you can attribute. Treat any referrer you did not build yourself as unattributable, log it in full, and let a server side deferred match handle that install instead.
How long do I have to read the Android install referrer?
Ninety days. Google's reference says the install referrer information "will be available for 90 days and won't change unless the application is reinstalled", and separately advises calling the API "only once during the first execution after install". A second read is wasted work, not a lost referrer.
Do I need the advertising id to use the Install Referrer API?
No. The referrer is stored against the install itself, so no device identifier is involved in retrieving it. Play's advertising id policy governs advertising uses, and apps targeting Android 13 or above that use the advertising id must declare the `AD_ID` permission. Our Android SDK reads no advertising id at all.
Why did my install return no referrer?
Most likely it was not a Play install. `getInstallReferrer()` reads information the Google Play Store app stored on the device, so a sideload, an `adb install`, an OEM pre-install or a rival store leaves nothing to read. A Play Store app too old for the API answers `FEATURE_NOT_SUPPORTED` instead.
Can I put a signed token in the referrer parameter?
Yes, within Google's limit of a URL encoded string of 512 characters or less. Percent encoding inflates anything containing `=` or `&`, so budget for it. We pack one parameter, `claim_token`, and resolve the creator, the campaign and the click on our own server instead.
Is the `install_referrer` broadcast still an option?
No. Google [deprecated the install_referrer intent broadcast](https://android-developers.googleblog.com/2019/11/still-using-installbroadcast-switch-to.html) on 1 March 2020 and said new versions of the Play Store app would stop broadcasting it after that date. The Play Install Referrer API replaced it, and nothing on the `InstallReferrerClient` reference is marked deprecated today.