Documentation
Android setup
App Links, the assetlinks.json file Google fetches, why you need every signing certificate rather than one, and the deterministic install match that iOS cannot do.
An Android App Link is an ordinary https:// address that opens your app when it is installed and a web page when it is not. Same idea as an iOS universal link, same address, and the same five lines of SDK code — the differences are all in the setup and in what happens after an install.
The three pieces
- A file at
/.well-known/assetlinks.jsonon your link host, naming the app and its signing certificates. We serve it. - An intent filter in your manifest with
android:autoVerify="true". - Code in your app to read the incoming URL and route on it.
Android checks that the first two agree, at install time. Neither side can claim the other unilaterally, which is what stops any app declaring itself the handler for somebody else’s domain.
The assetlinks file
Enter your package name and your signing certificate fingerprints on the app’s Android settings page, and this is what we publish:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.northwind.retail",
"sha256_cert_fingerprints": [
"AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89",
"12:34:56:78:9A:BC:DE:F0:12:34:56:78:9A:BC:DE:F0:12:34:56:78:9A:BC:DE:F0:12:34:56:78:9A:BC:DE:F0"
]
}
}
]Note the outermost brackets. Unlike Apple’s file this one is a JSON array, and serving an object instead is the commonest way to get it wrong. You are not typing it, so this is only worth knowing when you are comparing ours against an example you found elsewhere.
You need every fingerprint, not one. This is where integrations fail.
Your app is signed with a different certificate in debug and in release, and again by Google Play App Signing if you use it — which is the default for apps published since 2021. App Links verify only for builds signed with a certificate listed in that file.
The failure is silent and it looks exactly like the product not working. The usual sequence: a developer adds their release fingerprint, installs a debug build to test it, taps the link, watches Chrome open, and concludes the setup is broken. Nothing is logged. Nothing is shown. There is no error to search for.
So the field takes a list, and you should expect to fill in three:
- Debug and release come from
keytool, below. - Play App Signing. Play Console → your app → Setup → App integrity. Copy the SHA-256 under “App signing key certificate”.
# Debug. This keystore is created for you by the Android tooling,
# and the password really is "android".
keytool -list -v \
-keystore ~/.android/debug.keystore \
-alias androiddebugkey -storepass android -keypass android
# Release, using your own upload keystore.
keytool -list -v -keystore your-release.keystoreTake the line beginning SHA256, not SHA1. If you paste a SHA-1 by mistake we tell you which one it is rather than saving it, and the validator on the Android settings page warns you while only one fingerprint is stored.
The manifest
One intent filter, on the activity that should open. The autoVerify attribute is what makes Android check the file at all — without it everything else is correct and links still open a browser.
<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="71k8c.qbrt.app" />
</intent-filter>
</activity>On a Capacitor or Cordova app this goes in android/app/src/main/AndroidManifest.xml, and the activity is already there — you are adding the intent-filter to it.
The SDK: the same five lines
Nothing here is Android-specific. This is the identical code from the Capacitor quickstart, and it is identical on purpose — there is no platform check for you to write and no Android-only option to pass.
import { QubeRoute } from '@quberoute/sdk'
const client = await QubeRoute.init({ key: 'qr_live_pk_…' })
client.onLink((link) => {
router.navigateByUrl(link.data.path as string)
})The SDK detects the platform and takes the Android path by itself. On a device with an install-referrer plugin present it reads the referrer Google hands the app and sends it with the one request it already makes; on iOS, on the web, and on an Android build without the plugin it sends nothing extra and behaves exactly as before.
The install referrer, which is why Android is better
When somebody taps your link without the app installed, we send them to your Play Store listing with an opaque token attached. Google keeps that token through the install and hands it to your app the first time it opens. Your app gives it back to us, and we know exactly which click produced the install.
Apple has no equivalent and has never built one. That single difference is the whole reason iOS attribution is an inference with a published false positive rate and Android attribution is a fact.
To read the referrer, add a plugin that exposes Google’s Install Referrer API. The SDK looks for the common ones and uses whichever it finds; it never throws if none is present, and a plugin that fails is reported through onError rather than taken as an emergency.
npm install capacitor-plugin-play-install-referrer
npx cap sync androidYou need a Play Store address saved on the app’s Android settings page for any of this to happen. Without one, an Android visitor without your app goes to the link’s web fallback instead of the Play Store, and there is no referrer for Google to keep — so the install is reported as unattributed.
How well it works, honestly
These two numbers measure different things and are not comparable. Saying “Android matches 100%” next to “iOS matches 40.6%” would be a sales claim rather than a measurement, so here is what each one is.
- iOS’s match rate measures our inference. Every signal is present and the question is whether we can pick the right click out of a crowd. We publish 40.6% matched at a 3.41% false positive rate, and 48.7% at 19.17% on one shared network with identical devices.
- Android’s measures Google’s delivery. Our inference is not involved at all: if the referrer arrives, the answer is exact. What we cannot measure from here is how often it arrives.
So we publish no Android match rate at all, and that is deliberate rather than an omission. What we could publish is the share of referrers that resolve once they reach us, and that number is 100% by construction — a token we minted, looked up by that token, in a unique index. Printing it beside iOS’s 40.6% would be putting an arithmetic certainty next to a sampled measurement and inviting you to compare them.
What the tests do establish, and what is worth your knowing, is that the referrer path cannot be wrong in the ways inference can: tests/db/android-referrer.test.ts holds it to attributing the exact click that minted the token, consuming it once so a copied referrer cannot attribute twice, and refusing a token belonging to another app or another environment — on the same crowded, identical-device population where iOS attributes roughly one install in five to the wrong link.
The half we cannot measure without production traffic is the referrer arriving at all. It does not when somebody reaches the Play Store another way, when the Play Store app is disabled or absent on the device, or when the install happens long after the tap. We will publish a measured delivery rate when we have real traffic to measure, and not before.
Things that cost people an afternoon
- Verification happens at install, not at tap. Correcting anything on our side changes nothing on a device that already has the app. Reinstall it, or ask the device to try again — both commands are below. This is the Android equivalent of Apple’s 24-hour cache, and it catches everybody once.
android:autoVerify="true"is easy to leave out. Everything on the validator passes, and links still open a browser.- On Android 12 and later there is no prompt. An unverified link opens the browser silently — the user is not asked which app to use, so there is nothing for you to notice.
- The package name is not always the manifest package. We need the
applicationIdfrom your app-levelbuild.gradle. Modern projects are allowed to set the two differently, and a mismatch here fails silently. - Typing the link into Chrome does not test anything. Like Safari on iOS, Chrome treats an address you typed as a web request. Tap it from another app instead.
- Testing the referrer takes a real Play Store install. Sideloading an APK produces no referrer, so deferred attribution cannot work in that test. Use an internal testing track.
# Each host this app claims, and whether it verified.
adb shell pm get-app-links your.package.name
# Ask it to check again, without reinstalling.
adb shell pm verify-app-links --re-verify your.package.nameChecking it
The app’s Android settings page in the dashboard runs the same three checks the iOS one does: your configuration, the file we actually serve (fetched, not asserted), and what Google’s statement list API can see for your host. It also warns while only one fingerprint is stored, because that is the failure this page opens with.
Next: deferred matching and how well it works, or when it does not work.
Ask the documentation
It answers from these pages only, and links what it used. If the answer is not here it says so rather than guessing — then email [email protected].