← Back to list

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:

Rajen Trivedi · 2026-07-30 18:03 · 0 claps · 8.2 min read
#android-development #kotlin-multiplatform #deep-linking #android-app-development #kotlin
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

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

Deep Linking Done Right

When a user opens https://xyz.com/word/hello on Android, I want the app to open directly to the hello word.

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


메타데이터
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