Android App Links: From https://xyz.com/word/hello to Your Android App
A few weeks ago, I wanted to solve a problem that sounded almost too easy to be worth a blog post:
Android App Links: From https://xyz.com/word/hello to Your Android App
A few weeks ago, I wanted to solve a problem that sounded almost too easy to be worth a blog post:

Deep Linking Done Right
When a user opens
https://xyz.com/word/helloon Android, I want the app to open directly to thehelloword.
The URL already existed. The website was already live. The Android app was already working.
So I assumed this would be a small Android configuration change.
It wasn’t.
The actual implementation is not especially complicated, but there are several pieces involved, and they all have to agree with each other. Once I understood how those pieces fit together, the setup became much easier to reason about.
This article walks through that process from the ground up: what Android App Links actually are, why assetlinks.json is required, where the Play App Signing SHA-256 comes from, how Nginx fits into the picture, how this works in a Kotlin Multiplatform project, and how to verify that everything is working.
1. Start with the Problem, Not the Configuration
Before writing any XML or Nginx rules, it helps to define what we actually want.
Imagine a website with URLs like:
https://xyz.com/word/hello
https://xyz.com/word/world
https://xyz.com/word/computer
When the app is not installed, these should behave like normal website URLs.
When the app is installed, I want Android to recognize that the app can handle those links and open the corresponding screen.
Conceptually:
User taps a URL
↓
Android recognizes the domain
↓
Android verifies the website ↔ app association
↓
The app opens
↓
The app reads the URL
↓
The app navigates to the requested content
That last part — reading the URL and navigating inside the app — is only one part of the problem.
The more interesting question is how Android knows that xyz.com actually trusts this application.
2. Android Needs Proof That the Website and App Belong Together
This is the part that made the whole feature click for me.
An Android app can declare that it wants to handle a URL, but that declaration alone does not prove ownership of the domain.
Android App Links solve this by creating a relationship between two things:
Android app Website
----------- -------
AndroidManifest.xml <------> assetlinks.json
The Android side says:
“I can handle links from this website.”
The website side says:
“I trust this Android application to handle my links.”
Android can then verify that both declarations match.
That website-side declaration is the Digital Asset Links file:
https://xyz.com/.well-known/assetlinks.json
Once I understood this two-sided model, the rest of the configuration felt much more logical.
3. Get the Correct SHA-256 Certificate
This is probably the easiest step to get wrong.
The assetlinks.json file needs the SHA-256 certificate fingerprint of the Android application that is being trusted.
For an app distributed through Google Play, the production application is signed through Google Play App Signing. That means the certificate we care about for the Play-distributed application is the app signing certificate, not simply the upload certificate.
In Play Console, you can find the certificate under the Play App Signing section:
Google Play Console
→ Protected with Play
→ Play Store protection
→ Manage Play app signing
→ App signing key
→ App signing key certificate
→ SHA-256 certificate fingerprint
Copy the SHA-256 fingerprint from there.
It will look something like:
AA:BB:CC:DD:EE:FF:...
One detail worth remembering: if you intentionally support multiple signing certificates, assetlinks.json can contain multiple SHA-256 fingerprints. But every fingerprint you add should represent a certificate you actually need to trust.
4. Create assetlinks.json
Now we can create the website-side association.
A minimal example looks like this:
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": [
"YOUR_PLAY_APP_SIGNING_SHA256"
]
}
}
]
There are only a few values here, but each one is important.
relation
This tells Android what relationship is being delegated. For normal App Links, the commonly used relation is:
"delegate_permission/common.handle_all_urls"
package_name
This must exactly match your Android application ID.
For example:
com.example.app
If the package name is wrong, Android cannot associate the file with your application.
sha256_cert_fingerprints
This contains the SHA-256 certificate fingerprint of the application that the website trusts.
For a Play-distributed production app, use the appropriate Play App Signing certificate.
5. Put the File in the Right Place
The filename and location are not optional.
Android expects:
https://xyz.com/.well-known/assetlinks.json
If your website is served from:
/var/www/website
then the file should physically exist at:
/var/www/website/.well-known/assetlinks.json
On a Linux server, you can create the directory with:
sudo mkdir -p /var/www/website/.well-known
Then create the file:
sudo nano /var/www/website/.well-known/assetlinks.json
At this point, the file exists on the server. But that still doesn’t mean Android can access it.
That’s where the web server configuration becomes important.
6. Nginx: Don’t Let Your SPA Eat assetlinks.json
Many modern websites are single-page applications, so Nginx often contains a fallback rule such as:
location / {
try_files $uri $uri.html /index.html =404;
}
That’s useful for normal application routes.
But imagine Android requests:
/.well-known/assetlinks.json
and Nginx cannot find the file.
The SPA fallback might decide to return:
/index.html
From a browser’s perspective, that may look like the site is working.
From Android’s perspective, it is completely wrong. Android asked for JSON and received HTML.
The simplest solution is to give assetlinks.json an exact-match location:
location = /.well-known/assetlinks.json {
default_type application/json;
try_files $uri =404;
}
Now there is no ambiguity about how this request should be handled.
If your website root is:
root /var/www/website;
then the complete relationship is:
Nginx root
└── .well-known
└── assetlinks.json
After changing Nginx, validate the configuration:
sudo nginx -t
If the test passes, reload Nginx:
sudo systemctl reload nginx
7. Test the Website Before Touching Android Again
This is one of the most useful debugging habits in the whole process.
Don’t immediately install the app and start wondering why verification failed.
First, test the website itself:
curl -i https://xyz.com/.well-known/assetlinks.json
A healthy response should look roughly like:
HTTP/1.1 200 OK
Content-Type: application/json
Then check the response body.
You should see the JSON you created, not your website’s HTML.
This single command can catch several problems immediately:
- the file is in the wrong directory
- Nginx is using a different
root - the request is falling through to an SPA route
- Nginx was not reloaded
- the file permissions are incorrect
- the JSON file is not publicly reachable
I would always get this part working before debugging the Android application.
8. Configure the Android Manifest
Now that the website is ready, the Android application needs to declare the links it can handle.
A typical App Links intent filter looks like this:
<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="xyz.com"
android:pathPrefix="/word" />
</intent-filter>
Let’s break that down.
android:scheme="https"
This tells Android that we’re dealing with normal HTTPS URLs.
android:host="xyz.com"
This is the website domain associated with the app.
android:pathPrefix="/word"
This restricts the links to the paths we care about.
For example:
https://xyz.com/word/hello
matches /word.
android:autoVerify="true"
This asks Android to verify the website association.
Without verification, you’re not getting the full Android App Links behavior you’re aiming for.
9. What Changes When You’re Using Kotlin Multiplatform?
Kotlin Multiplatform introduces a small architectural question: where should the deep-link configuration live?
The answer is simple once you separate platform concerns from shared logic.
The Android intent-filter belongs in the Android part of the project.
It should not be treated as a commonMain configuration because the Android manifest is an Android-specific mechanism.
A useful way to think about the responsibilities is:
Android-specific
----------------
Manifest
Intent filter
App Link verification
Shared KMP
----------
URL parsing
Navigation decisions
Business logic
Screen state
Android receives the external URL first. Once your application has that URL, the shared code can take over the parts that make sense to share.
10. What About Google Play Console?
One question that naturally comes up is whether there is another deep-link configuration that needs to be uploaded to Google Play.
For the normal Android App Links setup, assetlinks.json remains a file hosted on your website.
Google Play Console also provides a Deep links area where you can inspect the app’s deep-link configuration and website mappings.
You can find it under:
Play Console
→ Grow users
→ Deep links
I see this as a useful place to validate what Google knows about the application’s deep links, but it doesn’t replace the Android manifest or the assetlinks.json file on your domain.
Those are still the important pieces.
11. Verify the Link Association from Android
Once the website and application are configured, it’s time to see what Android thinks.
With ADB connected to a device or emulator, you can inspect the app-link state with:
adb shell pm get-app-links com.example.app
You can also request another verification pass:
adb shell pm verify-app-links --re-verify com.example.app
Replace com.example.app with the actual application ID.
The exact command output depends on the Android version, but the goal is to see the domain recognized as verified.
At that point, test the real URL:
https://xyz.com/word/hello
The Android system should be able to associate the link with the application.
12. A Debugging Order That Saves Time
If something doesn’t work, I would avoid changing five things at once.
Check the pieces in this order:
Step 1 — The URL
Does this exist?
https://xyz.com/word/hello
Step 2 — assetlinks.json
Does this return JSON?
https://xyz.com/.well-known/assetlinks.json
Step 3 — Package name
Does the package_name in assetlinks.json exactly match the Android application ID?
Step 4 — Certificate
Does the SHA-256 fingerprint correspond to the certificate used by the installed build?
For a Play-installed production app, verify the Play App Signing certificate.
Step 5 — Manifest
Does the intent filter use the correct scheme, host, path, and android:autoVerify="true"?
Step 6 — Device verification
Does Android report the domain as verified?
This order matters because it moves from the outside world toward the application. If the public association file is broken, there is no reason to start debugging navigation code.
13. What I Would Check First When It Fails
Here are the failures I would expect most often.
assetlinks.json returns 404
Check that the file actually exists:
ls -l /var/www/website/.well-known/assetlinks.json
Then test Nginx:
sudo nginx -t
assetlinks.json returns HTML
This usually means an SPA fallback or another Nginx location handled the request.
Make sure the exact-match location exists:
location = /.well-known/assetlinks.json {
default_type application/json;
try_files $uri =404;
}
The file looks correct but verification fails
Check the two most sensitive values:
package_name
sha256_cert_fingerprints
One character being wrong is enough to break the association.
It works with one APK but not another
Check how each APK was signed.
A local/debug build and a Play-distributed build can use different certificates. The fingerprint in assetlinks.json needs to correspond to the application Android is actually verifying.
Everything is correct but Android still behaves as if nothing changed
Remember that domain verification state can be cached. Re-running verification with ADB can be useful during development.
14. The Complete Picture
After putting all the pieces together, the architecture looks like this:
https://xyz.com/word/hello
│
▼
┌──────────────────────┐
│ Android Manifest │
│ │
│ https + xyz.com │
│ /word + autoVerify │
└──────────┬───────────┘
│
│ verification
▼
┌──────────────────────┐
│ Nginx / Web Server │
│ │
│ /.well-known/ │
│ assetlinks.json │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Digital Asset Links │
│ │
│ package name │
│ SHA-256 certificate │
└──────────────────────┘
Every layer has a small job.
The manifest declares the link.
The website publishes the trust relationship.
The certificate identifies the signed application.
Android verifies that everything matches.
And only then does the ordinary HTTPS URL become a verified App Link.
15. Final Thoughts
What initially looked like a one-line feature request turned out to be a nice example of how several small systems work together.
There isn’t one magic setting for deep linking.
There is a contract:
The app says:
"I handle this domain."
The website says:
"I trust this app."
Android says:
"Both sides agree."
Once that clicked for me, the setup stopped feeling like a collection of unrelated configuration files.
And that’s probably the biggest lesson I took away from implementing Android App Links: when a feature seems unnecessarily complicated, it is often worth stepping back and understanding the relationship between the pieces before touching more configuration.
For this particular problem, that mental model made everything else much easier.
References
- Android App Links: https://developer.android.com/training/app-links/about
- Configure Digital Asset Links: https://developer.android.com/training/app-links/configure-assetlinks
- Verify App Links: https://developer.android.com/training/app-links/verify-applinks
- Troubleshoot App Links: https://developer.android.com/training/app-links/troubleshoot
- Google Play Console deep links: https://support.google.com/googleplay/android-developer/answer/12463044
메타데이터
- post_id
- f2d70644e72b
- slug
- android-app-links-from-https-xyz-com-word-hello-to-your-android-app-f2d70644e72b
- url
- https://medium.com/@rajen-trivedi/android-app-links-from-https-xyz-com-word-hello-to-your-android-app-f2d70644e72b
- canonical_url
- https://medium.com/@rajen-trivedi/android-app-links-from-https-xyz-com-word-hello-to-your-android-app-f2d70644e72b
- author_url
- https://medium.com/@rajen-trivedi
- status
- ok
- fetched_at
- 2026-08-25 23:57:55