Linkzly

Smart Links — Android Setup Guide

---

19 min read

sidebar_label: Smart Links — Android sidebar_category: Smart Links Integration

Smart Links — Android Setup Guide

This guide covers everything you need to do inside your Android app to support Linkzly Smart Links (called Deeplinks in the dashboard). When a user taps a Smart Link on Android, the goal is for your app to open directly to the correct screen. If the app is not installed, Linkzly redirects to the Google Play Store, and after the user installs and opens the app, Linkzly delivers the original deep link destination — this is called deferred deep linking.

What Linkzly does automatically:

  • Hosts the Digital Asset Links file at https://<your-domain>/.well-known/assetlinks.json
  • Handles deferred deep link storage and delivery
  • Routes uninstalled users to the Google Play URL you configured
  • Tracks clicks, installs, and deep link delivery events

What you must do in your Android project:

  • Add your SHA-256 certificate fingerprint(s) to the Linkzly app registration
  • Add an intent filter with android:autoVerify="true" in AndroidManifest.xml for your Linkzly domain
  • Handle the incoming intent in your Activity and parse the URI to navigate to the correct screen

Prerequisites

Before starting the Android configuration, complete the app registration in the Linkzly console. The Android setup requires the following:

Requirement Where to Find It
Package Name applicationId in your app-level build.gradle. Example: com.yourcompany.yourapp
SHA-256 Certificate Fingerprint From your debug keystore, release keystore, or Google Play Console (see Step 1)
Linkzly domain Linkzly Console → Apps → [Your App] → the hosted subdomain or your custom domain. Example: yourapp.linkz.ly
Smart Link (Deeplink) At least one Smart Link created and in Active status in Linkzly Console → Deeplink

If you have not yet registered your app in Linkzly, follow the steps in the Smart Links overview first — specifically Step 1 (Register an App) and Step 3 (Create a Deeplink). Come back here once you have your Linkzly domain and at least one Smart Link created.


Step 1 — Get Your SHA-256 Certificate Fingerprint

Android App Links require that your app's signing certificate fingerprint is listed in the assetlinks.json file that Linkzly serves. The fingerprint must match the certificate used to sign the APK installed on the device.

You will add this fingerprint to the Linkzly console so Linkzly can include it in the assetlinks.json file automatically.

Debug Keystore

For development builds signed with the default Android debug keystore:

keytool -list -v \
  -keystore ~/.android/debug.keystore \
  -alias androiddebugkey \
  -storepass android \
  -keypass android

Release Keystore

For production builds signed with your release keystore:

keytool -list -v \
  -keystore /path/to/your-release.keystore \
  -alias your-key-alias \
  -storepass your-store-password

Both commands output a block containing:

Certificate fingerprints:
  SHA1:   AA:BB:CC:...
  SHA256: AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99

Copy the SHA-256 value — the colon-separated 64-character hex string.

Google Play App Signing

If you use Google Play App Signing (the default for new apps), Google re-signs your APK with its own key after you upload it. The SHA-256 fingerprint you add to Linkzly must match Google's signing key, not your upload key — they are different.

  1. Open Google Play Console → Your App → Setup → App integrity.
  2. Under App signing key certificate, copy the SHA-256 certificate fingerprint shown there.

Important: If you use Google Play App Signing and add only your local keystore fingerprint, App Links verification will fail on production builds distributed through the Play Store. Always add the fingerprint from the Google Play Console for production.

You can add multiple fingerprints — for example, one for your debug keystore and one from the Google Play Console. Linkzly includes all registered fingerprints in the assetlinks.json file.


Step 2 — Add the Fingerprint to Linkzly

  1. Go to Linkzly Console → Apps.
  2. Click on your registered Android app.
  3. In the SHA-256 Fingerprints section, paste the fingerprint you copied in Step 1.
  4. Click Add Fingerprint. Repeat for each additional fingerprint (debug, release, Play signing key).
  5. Click Save.

Linkzly immediately updates the assetlinks.json file on your domain to include the new fingerprint.


Step 3 — Verify the assetlinks.json File

Linkzly hosts the Digital Asset Links file for you automatically. Verify it is reachable and contains the correct values by running this command in Terminal, replacing the domain with yours:

curl https://yourapp.linkz.ly/.well-known/assetlinks.json

The response will be a JSON array with a structure similar to:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.yourcompany.yourapp",
      "sha256_cert_fingerprints": [
        "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
      ]
    }
  }
]

Check that:

  • package_name matches your app's package name exactly (case-sensitive).
  • sha256_cert_fingerprints contains the fingerprint you added, including the colons.

If the file returns a 404 or an HTML error page, allow up to 5 minutes after saving your app registration in Linkzly and try again. For custom domains, verify your DNS CNAME record points to Linkzly's servers and has propagated.


Step 4 — Configure AndroidManifest.xml

Add an <intent-filter> with android:autoVerify="true" to the Activity that should handle your Smart Links. The autoVerify attribute instructs Android to verify App Link ownership at install time by fetching the assetlinks.json file from your domain.

<activity
    android:name=".MainActivity"
    android:launchMode="singleTask"
    android:exported="true">

    <!-- Default launcher intent filter — keep your existing entry point -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <!-- App Links intent filter — handles Linkzly Smart Links -->
    <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="yourapp.linkz.ly" />
    </intent-filter>

</activity>

Replace yourapp.linkz.ly with your Linkzly hosted subdomain or custom domain.

If you have Smart Links on more than one domain, add a separate <intent-filter> block for each domain:

<!-- If using a custom domain in addition to the hosted subdomain -->
<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="go.yourcompany.com" />
</intent-filter>

Key points:

  • android:autoVerify="true" is required. Without it, Android shows a disambiguation dialog instead of opening your app directly.
  • android:launchMode="singleTask" ensures the activity is not started twice if the app is already in the foreground when the link is tapped.
  • android:exported="true" is required on Android 12 (API 31) and above for any activity with an intent filter.
  • Only add https — Linkzly Smart Links always use HTTPS. You do not need an http variant.

Step 5 — Handle the Incoming Intent in Your Activity

Override onCreate() and onNewIntent() in the activity you declared in the manifest. Both methods need to call your intent handler so the deep link is processed whether the app was already running or launched fresh.

Kotlin Activity with Intent Parsing

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Handle the App Link intent that launched this activity
        handleDeepLinkIntent(intent)
    }

    // Called when the app is already running and receives a new App Link
    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        handleDeepLinkIntent(intent)
    }

    private fun handleDeepLinkIntent(intent: Intent) {
        if (intent.action != Intent.ACTION_VIEW) return

        val uri: Uri = intent.data ?: return

        // Extract the deep_link_path query param set in the Linkzly dashboard.
        // Fall back to the raw URI path if the param is not present.
        val deepLinkPath: String = uri.getQueryParameter("deep_link_path")
            ?: uri.path
            ?: "/"

        // Collect any additional query parameters as supplementary data
        val params: Map<String, String> = uri.queryParameterNames
            .filter { it != "deep_link_path" }
            .associateWith { name -> uri.getQueryParameter(name) ?: "" }

        route(path = deepLinkPath, params = params)
    }

    private fun route(path: String, params: Map<String, String>) {
        // Normalize the path and split into segments
        val segments = path
            .trimStart('/')
            .split("/")
            .filter { it.isNotEmpty() }

        when (segments.firstOrNull()) {
            "product" -> {
                // deep_link_path = /product/456
                val productId = segments.getOrElse(1) { "" }
                openProductScreen(productId, params)
            }
            "promo" -> {
                // deep_link_path = /promo/summer-sale
                val slug = segments.getOrElse(1) { "" }
                openPromoScreen(slug)
            }
            "profile" -> {
                // deep_link_path = /profile/user123
                val userId = segments.getOrElse(1) { "" }
                openProfileScreen(userId)
            }
            "onboarding" -> {
                // deep_link_path = /onboarding  +  data param: step=welcome
                val step = params["step"] ?: "welcome"
                openOnboardingScreen(step)
            }
            "article" -> {
                // deep_link_path = /article/789
                val articleId = segments.getOrElse(1) { "" }
                openArticleScreen(articleId)
            }
            else -> openHomeScreen()
        }
    }

    // --- Navigation helpers — replace with your actual navigation code ---

    private fun openProductScreen(productId: String, params: Map<String, String>) {
        val intent = Intent(this, ProductActivity::class.java).apply {
            putExtra("product_id", productId)
            putExtra("campaign", params["campaign"])
        }
        startActivity(intent)
    }

    private fun openPromoScreen(slug: String) {
        val intent = Intent(this, PromoActivity::class.java).apply {
            putExtra("promo_slug", slug)
        }
        startActivity(intent)
    }

    private fun openProfileScreen(userId: String) {
        val intent = Intent(this, ProfileActivity::class.java).apply {
            putExtra("user_id", userId)
        }
        startActivity(intent)
    }

    private fun openOnboardingScreen(step: String) {
        val intent = Intent(this, OnboardingActivity::class.java).apply {
            putExtra("start_step", step)
        }
        startActivity(intent)
    }

    private fun openArticleScreen(articleId: String) {
        val intent = Intent(this, ArticleActivity::class.java).apply {
            putExtra("article_id", articleId)
        }
        startActivity(intent)
    }

    private fun openHomeScreen() {
        // Already on home — no navigation needed, or navigate to HomeActivity
    }
}

Step 6 — Jetpack Compose Navigation

If your app uses Jetpack Compose with NavController, handle the incoming intent in your MainActivity and pass the deep link URI to the navigation host. The Compose Navigation library has built-in support for parsing deep links declared in your nav graph, but you can also extract parameters manually for full control.

Using Compose Navigation with Deep Link Declarations

Declare the deep link on each destination in your nav graph:

import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink

@Composable
fun AppNavHost(navController: NavHostController) {
    NavHost(navController = navController, startDestination = "home") {

        composable("home") {
            HomeScreen()
        }

        composable(
            route = "product/{productId}",
            arguments = listOf(navArgument("productId") { type = NavType.StringType }),
            deepLinks = listOf(navDeepLink {
                uriPattern = "https://yourapp.linkz.ly/abc123?deep_link_path=/product/{productId}"
            })
        ) { backStackEntry ->
            val productId = backStackEntry.arguments?.getString("productId") ?: ""
            ProductDetailScreen(productId = productId)
        }

        composable(
            route = "promo/{slug}",
            arguments = listOf(navArgument("slug") { type = NavType.StringType }),
            deepLinks = listOf(navDeepLink {
                uriPattern = "https://yourapp.linkz.ly/abc123?deep_link_path=/promo/{slug}"
            })
        ) { backStackEntry ->
            val slug = backStackEntry.arguments?.getString("slug") ?: ""
            PromoScreen(slug = slug)
        }

        composable(
            route = "profile/{userId}",
            arguments = listOf(navArgument("userId") { type = NavType.StringType }),
            deepLinks = listOf(navDeepLink {
                uriPattern = "https://yourapp.linkz.ly/abc123?deep_link_path=/profile/{userId}"
            })
        ) { backStackEntry ->
            val userId = backStackEntry.arguments?.getString("userId") ?: ""
            ProfileScreen(userId = userId)
        }
    }
}

Manual Compose Navigation via Intent Handling

For more control — particularly when Linkzly passes deep_link_path as a query parameter rather than a path segment — handle the intent manually in MainActivity and call navController.navigate() directly:

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.remember
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController

class MainActivity : ComponentActivity() {

    private var navController: NavHostController? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val controller = rememberNavController()
            navController = controller

            AppNavHost(navController = controller)
        }

        // Process the intent that launched the activity
        handleDeepLinkIntent(intent)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        handleDeepLinkIntent(intent)
    }

    private fun handleDeepLinkIntent(intent: Intent) {
        if (intent.action != Intent.ACTION_VIEW) return

        val uri: Uri = intent.data ?: return

        val deepLinkPath = uri.getQueryParameter("deep_link_path") ?: uri.path ?: "/"
        val params = uri.queryParameterNames
            .filter { it != "deep_link_path" }
            .associateWith { uri.getQueryParameter(it) ?: "" }

        // Dispatch navigation after the Compose content is ready
        navController?.let { controller ->
            navigateFromPath(controller, deepLinkPath, params)
        }
    }

    private fun navigateFromPath(
        navController: NavHostController,
        path: String,
        params: Map<String, String>
    ) {
        val segments = path.trimStart('/').split("/").filter { it.isNotEmpty() }

        when (segments.firstOrNull()) {
            "product" -> {
                val productId = segments.getOrElse(1) { "" }
                navController.navigate("product/$productId")
            }
            "promo" -> {
                val slug = segments.getOrElse(1) { "" }
                navController.navigate("promo/$slug")
            }
            "profile" -> {
                val userId = segments.getOrElse(1) { "" }
                navController.navigate("profile/$userId")
            }
            "onboarding" -> {
                val step = params["step"] ?: "welcome"
                navController.navigate("onboarding?step=$step")
            }
            else -> navController.navigate("home")
        }
    }
}

Step 7 — Deferred Deep Linking

When a user taps a Smart Link but does not have your app installed, Linkzly redirects them to the Google Play Store URL you configured. Linkzly handles the deferred deep link storage on its side.

After the user installs the app from the Play Store and opens it for the first time, Linkzly delivers the original deep link through the Play Install Referrer API. The referrer string contains the original Smart Link destination URL, which your app reads on first launch and uses to navigate the user to the correct screen.

Deferred Deep Link Handling with Play Install Referrer

Add the Play Install Referrer library to your app-level build.gradle:

dependencies {
    implementation 'com.android.installreferrer:installreferrer:2.2'
}

Read the referrer on first launch in your MainActivity. Guard this behind a one-time flag stored in SharedPreferences so it only runs once — on the very first app open after install:

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerStateListener
import com.android.installreferrer.api.ReferrerDetails

class MainActivity : AppCompatActivity() {

    private lateinit var referrerClient: InstallReferrerClient

    companion object {
        private const val PREFS_NAME = "linkzly_prefs"
        private const val KEY_REFERRER_PROCESSED = "referrer_processed"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Handle a direct App Link intent first (takes priority)
        if (intent.action == Intent.ACTION_VIEW && intent.data != null) {
            handleDeepLinkIntent(intent)
        } else {
            // Otherwise, check for a deferred deep link from the Play Install Referrer
            checkDeferredDeepLink()
        }
    }

    private fun checkDeferredDeepLink() {
        val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
        if (prefs.getBoolean(KEY_REFERRER_PROCESSED, false)) {
            // Already processed referrer on a previous launch — skip
            return
        }

        referrerClient = InstallReferrerClient.newBuilder(this).build()
        referrerClient.startConnection(object : InstallReferrerStateListener {

            override fun onInstallReferrerSetupFinished(responseCode: Int) {
                if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
                    val details: ReferrerDetails = referrerClient.installReferrer
                    val referrerUrl = details.installReferrer

                    // Mark as processed so this only runs once
                    prefs.edit().putBoolean(KEY_REFERRER_PROCESSED, true).apply()

                    if (referrerUrl.isNotEmpty()) {
                        // Linkzly encodes the original destination in the referrer URL
                        handleDeferredDeepLink(referrerUrl)
                    }
                }
                referrerClient.endConnection()
            }

            override fun onInstallReferrerServiceDisconnected() {
                // Connection to Play Services dropped — safe to ignore for deferred links
            }
        })
    }

    private fun handleDeferredDeepLink(referrerUrl: String) {
        try {
            val uri = Uri.parse(referrerUrl)
            val deepLinkPath = uri.getQueryParameter("deep_link_path") ?: uri.path ?: return
            val params = uri.queryParameterNames
                .filter { it != "deep_link_path" }
                .associateWith { uri.getQueryParameter(it) ?: "" }

            runOnUiThread {
                route(path = deepLinkPath, params = params)
            }
        } catch (e: Exception) {
            // Referrer URL was not a valid Linkzly deep link — ignore
        }
    }

    private fun handleDeepLinkIntent(intent: Intent) {
        val uri: Uri = intent.data ?: return
        val deepLinkPath = uri.getQueryParameter("deep_link_path") ?: uri.path ?: "/"
        val params = uri.queryParameterNames
            .filter { it != "deep_link_path" }
            .associateWith { uri.getQueryParameter(it) ?: "" }
        route(path = deepLinkPath, params = params)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        if (intent.action == Intent.ACTION_VIEW) {
            handleDeepLinkIntent(intent)
        }
    }

    private fun route(path: String, params: Map<String, String>) {
        val segments = path.trimStart('/').split("/").filter { it.isNotEmpty() }

        when (segments.firstOrNull()) {
            "product" -> openProductScreen(segments.getOrElse(1) { "" }, params)
            "promo"   -> openPromoScreen(segments.getOrElse(1) { "" })
            "profile" -> openProfileScreen(segments.getOrElse(1) { "" })
            "onboarding" -> openOnboardingScreen(params["step"] ?: "welcome")
            "article" -> openArticleScreen(segments.getOrElse(1) { "" })
            else -> openHomeScreen()
        }
    }

    // --- Navigation helpers ---
    private fun openProductScreen(productId: String, params: Map<String, String>) { /* ... */ }
    private fun openPromoScreen(slug: String) { /* ... */ }
    private fun openProfileScreen(userId: String) { /* ... */ }
    private fun openOnboardingScreen(step: String) { /* ... */ }
    private fun openArticleScreen(articleId: String) { /* ... */ }
    private fun openHomeScreen() { /* ... */ }
}

Requirement: For deferred deep linking to work, the Google Play URL field in your Linkzly app registration must contain a valid Play Store listing URL for your app. This is the URL Linkzly redirects uninstalled users to so they can install the app.


Testing

Simulate an App Link with ADB

Use adb to simulate tapping a Smart Link with the app installed:

adb shell am start \
  -W \
  -a android.intent.action.VIEW \
  -d "https://yourapp.linkz.ly/abc123?deep_link_path=%2Fproduct%2F456&campaign=summer" \
  com.yourcompany.yourapp

Replace the URL with one of your actual Smart Links from the Linkzly console, and replace com.yourcompany.yourapp with your package name. If the intent filter is configured correctly, Android opens your activity and handleDeepLinkIntent() is called.

Verify App Link Verification Status

After installing the app, check whether Android successfully verified the App Link association:

# <span id="trigger-re-verification-useful-after-updating-assetlinksjson"></span>Trigger re-verification (useful after updating assetlinks.json)
adb shell pm verify-app-links --re-verify com.yourcompany.yourapp

# <span id="wait-510-seconds-then-check-the-result"></span>Wait 5–10 seconds, then check the result
adb shell pm get-app-links com.yourcompany.yourapp

The output lists each declared domain and its verification status. A correctly configured setup shows:

com.yourcompany.yourapp:
  ID: ...
  Signatures: [...]
  Domain verification state:
    yourapp.linkz.ly: verified

If the status shows none, ask, or legacy_failure, see the Troubleshooting section below.

Test Deferred Deep Linking

You can simulate a deferred deep link install by passing a referrer value via ADB on a device:

# <span id="simulate-the-play-store-referrer-that-linkzly-would-set"></span>Simulate the Play Store referrer that Linkzly would set
adb shell am broadcast \
  -a com.android.vending.INSTALL_REFERRER \
  -n com.yourcompany.yourapp/.InstallReferrerReceiver \
  --es "referrer" "https%3A%2F%2Fyourapp.linkz.ly%2Fabc123%3Fdeep_link_path%3D%2Fproduct%2F456"

For a more realistic test, uninstall the app, tap a Smart Link on the device, allow Linkzly to redirect you to the Play Store, then reinstall the app from the Play Store and open it. The referrer flow depends on Google Play Services and may not always be testable reliably on emulators.

Validate the assetlinks.json File

Use Google's Digital Asset Links API to validate your file before testing on a device:

# <span id="check-if-android-would-verify-your-domain-for-your-app"></span>Check if Android would verify your domain for your app
curl "https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://yourapp.linkz.ly&relation=delegate_permission/common.handle_all_urls"

Alternatively, use the visual tool at developers.google.com/digital-asset-links/tools/generator — enter your domain and package name to see if the file is valid and the fingerprint matches.


Troubleshooting

Android shows a disambiguation dialog instead of opening the app directly

  1. Confirm android:autoVerify="true" is set on the intent filter in AndroidManifest.xml.
  2. Check App Link verification with adb shell pm get-app-links com.yourcompany.yourapp — the domain should show verified.
  3. Verify the assetlinks.json file is accessible via curl and the package_name and sha256_cert_fingerprints values are correct.
  4. Reinstall the app. Android verifies App Links at install time. Re-verification can also be triggered with adb shell pm verify-app-links --re-verify.

Verification status shows none or legacy_failure

Common causes:

  • The SHA-256 fingerprint in assetlinks.json does not match the certificate used to sign the installed APK. If you use Google Play App Signing, use the fingerprint from the Play Console, not your local keystore.
  • The assetlinks.json file returned a non-200 HTTP status, an empty body, or invalid JSON at verification time. Test with curl.
  • There is a typo in package_name in assetlinks.json. It must match your app's applicationId exactly.
  • The device could not reach the assetlinks.json URL at install time. This can happen in restrictive network environments or on older Android versions.

App Link verification fails only on Play Store builds

This is almost always the Google Play App Signing fingerprint mismatch. Open Google Play Console → Setup → App integrity → App signing key certificate and add that SHA-256 fingerprint to your Linkzly app registration in addition to your local keystore fingerprint. Both will be included in assetlinks.json.

onNewIntent is not called — app restarts instead

Set android:launchMode="singleTask" (or singleTop) on your activity in the manifest. Without this, Android may launch a second instance of the activity when a new App Link arrives while the app is running, causing onCreate to be called instead of onNewIntent.

Deferred deep link referrer is empty

The Play Install Referrer is only populated when the user was redirected to the Play Store from a Smart Link. Direct Play Store installs (from search, browsing, or being featured) do not carry a referrer. Always check that the referrer string is non-empty before parsing it, and do not display an error to users when it is empty — this is the normal case for organic installs.

Deep link path extracted incorrectly

Log the full incoming URI in your intent handler to inspect what Linkzly is sending:

private fun handleDeepLinkIntent(intent: Intent) {
    Log.d("DeepLink", "Received URI: ${intent.data}")
    // ...
}

Verify the deep_link_path query parameter is present. In the Linkzly console, check the Smart Link's Deep Link Path field — the Advanced link type is required for path and data parameters to be forwarded to the app. Basic links do not include deep_link_path.

assetlinks.json cached stale values

Android caches the assetlinks.json file. After updating your app registration in Linkzly, force a fresh verification on the device:

adb shell pm verify-app-links --re-verify com.yourcompany.yourapp

This bypasses the device cache and fetches a fresh copy from the server.


Setup Checklist

  • App registered in Linkzly console with the correct Package Name.
  • SHA-256 fingerprint(s) added to the app registration — both debug keystore and the Google Play signing key fingerprint (if using Play App Signing).
  • assetlinks.json verified at https://<your-domain>/.well-known/assetlinks.jsonpackage_name and sha256_cert_fingerprints are correct.
  • <intent-filter android:autoVerify="true"> added to AndroidManifest.xml for each Linkzly domain.
  • android:launchMode="singleTask" set on the activity in AndroidManifest.xml.
  • android:exported="true" set on the activity (required on Android 12+).
  • onCreate() calls handleDeepLinkIntent(intent).
  • onNewIntent() calls handleDeepLinkIntent(intent).
  • deep_link_path query parameter extracted and used for screen routing.
  • All expected deep link paths handled in the router — unknown paths fall back gracefully.
  • App Link verification confirmed with adb shell pm get-app-links — domain shows verified.
  • Tested with adb shell am start — correct screen opens with expected parameters.
  • Deferred deep link handling implemented with the Play Install Referrer library and guarded with a one-time processed flag.
  • Google Play URL set in Linkzly app registration for the deferred deep link redirect to work.

Was this helpful?

Help us improve our documentation