Android deep linking with App Links that actually verify.
An App Link that fails verification is just a URL with an app chooser attached. Ferry publishes the assetlinks.json Android checks, routes everyone else to Play, and carries the destination through the install.
Android deep linking uses App Links: https URLs your app claims with an autoVerify intent filter and Android verifies against an assetlinks.json file on the hostname. When verification passes, the link opens your app with no disambiguation dialog. When the app is not installed, the same URL goes to Play, and deferred deep linking is what carries the destination through that install. Ferry publishes assetlinks.json per hostname, routes to the store, and reads the Play Install Referrer on first launch to return the original payload.
App Links, assetlinks.json, and the install gap.
Android verifies the relationship between your app and the domain at install time. Get any part of it wrong and the link still works, it just stops opening your app.
- Declare an autoVerify intent filter
Add an intent-filter with android:autoVerify set to true, the VIEW action, the DEFAULT and BROWSABLE categories, and one data entry per hostname. Without autoVerify, Android treats your app as one option among many and shows a chooser.
- Serve assetlinks.json on the hostname
The domain must return /.well-known/assetlinks.json listing your package name and the SHA-256 fingerprint of the signing certificate. Ferry publishes that file from project settings for every hostname assigned to the project.
- Use the fingerprint users actually get
Play App Signing re-signs your app, so the certificate on a user device is the Play one, not your upload key. Take the SHA-256 from App integrity in the Play Console, and add your debug fingerprint too if you test sideloaded builds.
- Forward the intent, twice
Call Ferry.handle(intent) in onCreate and again in onNewIntent after setIntent. With launchMode singleTask a running activity receives the new intent instead of starting a second task, which is what stops a link from opening a duplicate screen.
- Let the referrer close the install gap
Ferry appends a click ID to the Play referrer, and the SDK reads the raw referrer string on first launch and sends it to the server. That path is deterministic, so the match is evidence rather than a guess, and the payload arrives through the same Ferry.onLink callback with isDeferred set to true.
class AcmeApp : Application() {
override fun onCreate() {
super.onCreate()
Ferry.configure(context = this, publicKey = "pk_live_xxx")
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Fires for a verified App Link open and for the deferred match on the
// first launch after a Play install.
Ferry.onLink { link ->
when (link.data["screen"]) {
"product" -> router.showProduct(link.data["product_id"], link.isDeferred)
else -> router.showHome()
}
}
Ferry.handle(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
Ferry.handle(intent)
}
} The SDK reads the Play Install Referrer, calls the match endpoint once per install, and delivers direct and deferred links through the same callback. Failures degrade to no link rather than crashing the app.
Verification is the whole game.
Almost every Android deep linking bug report is really a verification failure, and Android checks it once at install time without telling you it went wrong.
| What goes wrong | What to do about it |
|---|---|
| assetlinks.json carries the upload key fingerprint. | Play App Signing replaces your signature, so the file has to list the Play certificate. A fingerprint from ./gradlew signingReport only matches debug and sideloaded builds, which is why it works on your device and fails in production. |
| Android shows a chooser instead of opening the app. | Run adb shell pm get-app-links on the package. The domain state tells you whether verification passed, failed, or never ran. Anything other than verified means Android could not tie the hostname to the installed signature. |
| A fix to the domain does not take effect. | Verification starts at install time, runs asynchronously, and only re-runs on its own when the app updates or a failed attempt is retried. Force a fresh pass with adb shell pm verify-app-links --re-verify your.package, or on Android 12 and later reset the state first with adb shell pm set-app-links --package your.package 0 all. |
| The same link opens two copies of a screen. | Set launchMode to singleTask and call setIntent(intent) inside onNewIntent before handing it to Ferry. Without setIntent, a warm start can replay the original launch intent. |
| Deferred matching returns nothing after a sideload. | The Play Install Referrer only exists for installs that came from Play. An ADB or sideloaded install carries no referrer, so matching falls back to a fingerprint or honestly returns no match. |
| An in-app browser swallows the URL. | Some apps open https links in their own webview before Android routes them. You cannot fix that from your side for every app, which is why the same Ferry URL always keeps a working store route and web fallback behind it. |
Questions developers ask first.
What is the difference between a deep link and an Android App Link? +
A deep link is any URI that routes into your app, including a custom scheme such as acme://. An Android App Link is an https deep link that Android has verified against an assetlinks.json file on the domain, which is what lets it open your app directly instead of showing a chooser dialog.
Why does my App Link show an app chooser? +
Verification failed, so Android treats your app as one of several apps that could handle the URL. Run adb shell pm get-app-links on the package and check the domain state. Anything other than verified means Android could not match assetlinks.json to the installed signature.
Do App Links work if the app is not installed? +
No. Android sends the visitor to whatever the URL serves in a browser. Ferry serves a Play Store route for that case, then matches the install back to the original link open through the Play Install Referrer.
How reliable is deferred deep linking on Android? +
More reliable than on iOS. The Play Install Referrer is a deterministic signal that Google passes through the install, so a match through it is proof rather than a probability. Ferry falls back to fingerprint matching only when the referrer carries no Ferry click ID.
Which signing certificate fingerprint belongs in assetlinks.json? +
The one users actually receive. With Play App Signing that is the SHA-256 from the App integrity page in the Play Console, not your local upload key. List both if you also test sideloaded or debug builds against the same hostname.
Which Android versions does the Ferry SDK support? +
API 24 and later. It depends only on AndroidX and the Play Install Referrer client, has no other third-party runtime dependencies, and accepts only a project pk_ public key.
Get App Links verifying today.
Claim a hostname, let Ferry publish assetlinks.json from your project settings, and test a real Play install end to end.