Skip to content

Set up deep links ​

Complete guide to setting up, verifying, and debugging deep links with Linkrunner for React Native and Flutter apps

Deep links allow users to navigate directly to specific content within your app by clicking on a URL. This guide walks you through the complete setup, from creating your verification config files and saving them in Linkrunner to making the necessary code changes in your app.

There are two primary approaches to deep linking:

  1. HTTP/HTTPS Deep Links: URLs with http:// or https:// protocols that can open your app when clicked. Requires domain verification.
  2. Custom URI Schemes: URLs with a custom protocol like myapp:// that are registered to your app. No verification needed.

HTTP/HTTPS Deep Linking ​

HTTP/HTTPS deep links (including App Links on Android and Universal Links on iOS) require you to prove domain ownership before they work reliably. The setup has four parts:

  1. Create your verification config files
  2. Save them in Linkrunner
  3. Update native configuration (Android & iOS)
  4. Configure your app's navigation

Step 1: Create Verification Config Files ​

Create a file named assetlinks.json with the following content:

json
[
    {
        "relation": ["delegate_permission/common.handle_all_urls"],
        "target": {
            "namespace": "android_app",
            "package_name": "your.package.name",
            "sha256_cert_fingerprints": ["SHA-256:XX:XX:XX:..."]
        }
    }
]

Replace:

  • your.package.name with your actual Android package name
  • SHA-256:XX:XX:XX:... with your app's SHA-256 fingerprint

Get your app's SHA-256 fingerprint ​

For debug builds:

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

For release builds:

bash
keytool -list -v -keystore your_release_keystore.keystore -alias your_key_alias

Look for the "SHA-256 Certificate fingerprint" line in the output.

Debug and release builds are signed with different keystores, so they produce different SHA-256 fingerprints. App Links verification will only succeed for the build whose fingerprint is currently saved in Linkrunner.

Before testing or shipping a release build, either:

  • Update the sha256_cert_fingerprints value in Linkrunner to your release keystore's fingerprint, or
  • List both fingerprints in the array so debug and release builds both verify:
json
"sha256_cert_fingerprints": [
    "SHA-256:DEBUG:FINGERPRINT:...",
    "SHA-256:RELEASE:FINGERPRINT:..."
]

If you use Google Play App Signing, get the release fingerprint from Play Console → Setup → App integrity, not your local upload keystore.

Step 2: Save Verification Config in Linkrunner ​

Linkrunner hosts your verification files automatically so you don't have to manage server configuration yourself.

  1. Open Settings → Deeplinks in your Linkrunner dashboard
  2. Paste your apple-app-site-association JSON into iOS Verification Object
  3. Paste your assetlinks.json content into Android Verification Object
  4. Click Save Changes

Linkrunner will automatically host these files at:

  • iOS: https://your-domain.io/.well-known/apple-app-site-association
  • Android: https://your-domain.io/.well-known/assetlinks.json

Step 3: Update Native Configuration ​

These changes are the same whether you're using React Native or Flutter.

Open android/app/src/main/AndroidManifest.xml and add the following inside the <activity> section:

xml
<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" />
  <!-- Your domain and subdomains -->
  <data android:scheme="https" android:host="example.com" />
  <data android:scheme="https" android:host="app.example.com" />
  <data android:scheme="https" android:host="store.example.com" />
</intent-filter>

Enable install detection (Continue to app popup) ​

To let the Linkrunner redirect page detect that your app is installed and show a Continue to app popup instead of sending the user to the store, declare an asset_statements link back to your subdomain.

Add the statement to android/app/src/main/res/values/strings.xml:

xml
<string name="asset_statements" translatable="false">
  [{\"relation\": [\"delegate_permission/common.handle_all_urls\"],
    \"target\": {\"namespace\": \"web\", \"site\": \"https://your.subdomain.com\"}}]
</string>

Then reference it from AndroidManifest.xml, inside the <application> section:

xml
<meta-data
  android:name="asset_statements"
  android:resource="@string/asset_statements" />

Replace https://your.subdomain.com with your Linkrunner subdomain. This is the app→site half of the Digital Asset Link. Linkrunner already hosts the site→app half at /.well-known/assetlinks.json on your subdomain.

Step 4: Configure Navigation ​

React Native uses React Navigation for handling deep links.

javascript
// App.js or your navigation configuration file
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import linkrunner from "rn-linkrunner";

const Stack = createStackNavigator();

function App() {
    const linking = {
        prefixes: [
            "https://example.com",
            "https://app.example.com",
            "https://store.example.com",
        ],
        config: {
            screens: {
                Home: "",
                Profile: "profile/:id",
                Store: {
                    path: "store/:category?",
                    parse: {
                        category: (category) => category || "all",
                    },
                },
                "app.example.com": {
                    screens: {
                        AppSpecificScreen: ":id",
                    },
                },
                "store.example.com": {
                    screens: {
                        StoreSpecificScreen: ":id",
                    },
                },
            },
        },
    };

    return (
        <NavigationContainer linking={linking}>
            <Stack.Navigator>{/* Your screens */}</Stack.Navigator>
        </NavigationContainer>
    );
}

export default App;

Custom URI Schemes ​

Custom URI schemes use a custom protocol like myapp:// and don't require domain verification. They're useful for backward compatibility or simpler setups.

Native Configuration ​

Open android/app/src/main/AndroidManifest.xml and add inside the <activity> section:

xml
<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="myapp" />
</intent-filter>

Use adb to test:

bash
# Test HTTP/HTTPS deep links
adb shell am start -a android.intent.action.VIEW -d "https://app.example.com/profile/123" your.package.name

# Test custom URI scheme
adb shell am start -a android.intent.action.VIEW -d "myapp://profile/123" your.package.name

If the HTTPS link opens the browser instead of your app, see Debugging Domain Verification.


Debugging Domain Verification ​

If an HTTPS link opens the browser instead of your app, domain verification is the usual cause. Work through these checks to find where it's failing.

Prefer to let your AI coding agent diagnose it? Run:

bash
npx @linkrunner/skills add deep-links

The skill runs the full gauntlet below automatically (hosted files, Apple CDN freshness, Android fingerprint + verification state, native wiring) and tells you exactly what is broken. See Linkrunner Agent Skills.

Check the hosted file

Confirm the file Linkrunner hosts for you is live and correct:

bash
curl https://your-domain.io/.well-known/assetlinks.json

If it returns a 404, your config isn't saved. Re-save it in Project Settings → Domain Verification. If it loads, check that package_name matches your app and sha256_cert_fingerprints includes the fingerprint of the keystore that signed the build you're testing.

You can also have Google's Digital Asset Links API validate it, which surfaces formatting errors:

bash
curl "https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://your-domain.io&relation=delegate_permission/common.handle_all_urls"

Check the verification state

On Android 12+, print the verification state of every domain in your manifest:

bash
adb shell pm get-app-links your.package.name
text
your.package.name:
    ID: 01234567-89ab-cdef-0123-456789abcdef
    Signatures: [F2:52:4D:82:...]
    Domain verification state:
      example.com: verified
      app.example.com: 1024
StateMeaning
verifiedVerification passed. Links for this domain open your app.
noneVerification hasn't run yet. It runs automatically about 20 seconds after install.
1024 (or higher) / legacy_failureVerification failed. Check the fingerprint and hosted file.
approvedManually approved, e.g. by the user in app settings.
deniedManually disallowed by the user in app settings.

On Android 11 and below, run adb shell dumpsys package domain-preferred-apps instead, and watch adb logcat | grep IntentFilter during install for verification messages.

Clear the cached state and re-verify

Android caches verification results, so fixing assetlinks.json has no effect until verification runs again. Clear the cached state and re-run it:

bash
# Clear the cached verification state for all domains
adb shell pm set-app-links --package your.package.name 0 all

# Re-run verification against the hosted assetlinks.json
adb shell pm verify-app-links --re-verify your.package.name

# Wait a few seconds, then check the result
adb shell pm get-app-links your.package.name

Reinstalling the app also triggers a fresh verification pass.

Force-approve the domain while debugging (optional)

To test your in-app navigation before verification passes, approve the domains manually. This is equivalent to the user enabling Open supported links in app settings:

bash
adb shell pm set-app-links-user-selection --user cur --package your.package.name true all

If links open your app after this but verification still fails, the problem is in the hosted file or fingerprint, not your app code.


Troubleshooting ​

Links open in the browser instead of the app (Android)
  • Check that the intent filter includes android:autoVerify="true" and lists the exact host you're testing.
  • Check the verification state and re-verify (see Debugging Domain Verification).
  • The SHA-256 fingerprint in Linkrunner must match the keystore that signed the build on your device. Debug and release builds have different fingerprints.
Works with a debug build but not the release build (Android)

Release builds are signed with a different keystore, so they have a different SHA-256 fingerprint. If you use Google Play App Signing, the production fingerprint comes from Play Console → Setup → App integrity, not your local keystore. Add the release fingerprint to sha256_cert_fingerprints in Linkrunner alongside the debug one.

Universal Links don't open the app (iOS)
  • Check that appID is TEAM_ID.BUNDLE_ID with the correct Team ID, and that the Associated Domains capability lists your domain.
  • Check what Apple's CDN is serving (see Debugging Domain Verification). If it's stale, use developer mode.
  • Don't type the URL into Safari. Universal links only trigger when tapped from another app.
I updated the verification files but nothing changed

Both platforms cache verification results:

  • Android: clear the cached state with adb shell pm set-app-links --package your.package.name 0 all, then re-verify with adb shell pm verify-app-links --re-verify your.package.name.
  • iOS: wait for Apple's CDN to refresh (up to a day) or enable developer mode, then delete and reinstall the app.
Custom URI scheme doesn't open the app

Check that the scheme is registered (intent filter on Android, URL Types in Xcode on iOS) and that the app is installed on the device. Use lowercase schemes everywhere; Android matches them case-sensitively.

Need help? Contact support@linkrunner.io