Deep Linking: Connecting the Web to Your Mobile App

Implement deep linking that seamlessly routes users from web links, emails, and notifications into specific screens in your mobile app. Covers Universal Links, App Links, deferred deep linking, link routing architectures, and the testing strategies that prevent deep links from silently breaking.

Deep linking connects the web and mobile apps by allowing a URL to open a specific screen or state within a mobile app, whether the app is installed or not. It matters when a user clicks a link from an email, a social post, or a web dashboard and expects to land directly in the app at the exact location—like a product detail, a user profile, or a checkout flow—without navigating manually.

When a user taps a URL, the system checks whether the corresponding mobile app is installed. If installed, the app opens and handles the URL. If not, the system may prompt to install the app from the App Store or Play Store, then open it after installation. This process is governed by a combination of URL schemes, universal links (iOS), and app links (Android), all working together to ensure a seamless transition.

URL Schemes

A URL scheme is the simplest form of deep linking. It uses a custom protocol, such as myapp://user/123. The mobile app registers this scheme during installation. On iOS, this registration happens in Info.plist:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.example.myapp</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
  </dict>
</array>

On Android, the same registration is done in AndroidManifest.xml:

<activity android:name=".MainActivity">
  <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>
</activity>

When a myapp://user/123 link is tapped, the system launches the app and passes the path and query parameters to it.

Universal links extend URL schemes by allowing the app to handle any https:// URL. The app declares a apple-app-site-association file at the root of its domain:

https://example.com/apple-app-site-association

This file is a JSON document, typically served with Content-Type: application/json, and contains a list of paths and patterns the app can handle:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "ABC123XYZ456.com.example.myapp",
        "paths": ["/user/*", "/product/*", "/orders", "*"]
      }
    ]
  }
}

The appID is the combination of the Apple Developer Team ID and the app’s bundle ID. The paths array supports wildcards (*) and exact paths. When the user taps a link like https://example.com/user/123, iOS checks for the apple-app-site-association file, fetches it, matches the path, and launches the app with the full URL as a query.

App links use assetlinks.json to establish trust between a website and a mobile app. The file is hosted at:

https://example.com/.well-known/assetlinks.json

It contains a list of assetlink objects, each describing a relationship between a web domain and an app package name:

[
  {
    "relation": ["delegate_default", "delegate"],
    "target": {
      "namespace": "android", 
      "package_name": "com.example.myapp",
      "sha256_cert_fingerprints": [
        "a1:b2:c3:d4:e5:f6:...:z1"
      ]
    }
  }
]

The sha256_cert_fingerprints field must match the app’s signing certificate. The relation field specifies how the app handles the link: delegate_default means the app handles the link and is the default handler for the domain.

Fallback Behavior

When a universal link or app link is available, the system attempts to open the app. If the app is not installed, it can fall back to the https:// URL, which may redirect to a mobile-optimized web version or trigger an install prompt.

On iOS, if the app is not installed and a universal link is available, the system downloads the app from the App Store and launches it with the URL. On Android, if the app is not installed, the system displays a “Install and Open” prompt.

Once the app is launched, it must parse the incoming URL and navigate to the appropriate screen. The app can be in one of three states: not running, in the background, or already running.

On App Launch

When the app starts from a deep link, the system passes the URL to the app’s entry point. On iOS, AppDelegate.swift receives the URL in application(_:didFinishLaunchingWithOptions:):

func application(_ app: UIApplication, 
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
  if let url = launchOptions?[.launchUrl] as? URL {
    handleDeepLink(url)
  }
  return true
}

On Android, MainActivity.onCreate() receives the intent data:

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  Intent intent = getIntent();
  Uri data = intent.getData();
  if (data != null) {
    handleDeepLink(data);
  }
}

The app can then extract parameters from the URL:

func handleDeepLink(_ url: URL) {
  let pathComponents = url.pathComponents
  let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
  
  switch pathComponents[1] {
  case "user":
    let userId = pathComponents[2]
    let user = User(id: userId, params: queryItems)
    showUserScreen(user)
  case "product":
    let productId = pathComponents[2]
    let product = Product(id: productId, params: queryItems)
    showProductScreen(product)
  default:
    showHomeScreen()
  }
}

When the app is already running, a deep link can arrive in the background. On iOS, application(_:continueUserActivity:restorationHandler:) handles incoming user activities:

func application(_ app: UIApplication, 
                 continue userActivity: NSUserActivity, 
                 restorationHandler: @escaping ([Any]) -> Void) -> Bool {
  if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
    handleDeepLink(userActivity.webpageURL!)
  }
  return true
}

On Android, onNewIntent() is called when the app receives a new intent while already running:

@Override
protected void onNewIntent(Intent intent) {
  super.onNewIntent(intent);
  Uri data = intent.getData();
  if (data != null) {
    handleDeepLink(data);
  }
}

The app must update its UI to reflect the new state and optionally add a back stack entry.

Query Parameters and State Management

Deep links often carry query parameters to control state. The query string is parsed and used to configure the app.

Common parameters include:

For example, https://example.com/product/123?tab=reviews&utm_source=email opens the product screen with the reviews tab selected, and tracks the source as email.

Handling Multiple Parameters

A single deep link can include multiple parameters. The app must combine them into a coherent state:

{
  "id": "123",
  "tab": "reviews",
  "sort": "newest",
  "filter": "featured"
}

The app can merge query parameters with existing state, such as a cached user profile or a navigation stack.

Fallback and Error Handling

If a deep link points to a non-existent resource, the app should handle it gracefully. For example, a user/999 link where user 999 does not exist:

If the URL is malformed, the app should parse it and fall back to defaults:

Testing deep links requires verifying the full flow: from a URL, through app install or launch, to screen rendering.

Testing on iOS

Use xcrun to test universal links:

xcrun simctl openurl booted https://example.com/user/123

This opens the app on the simulator and passes the URL. Use xcrun simctl log to inspect logs:

xcrun simctl log booted --filter "MyApp"

Testing on Android

Use adb to send intents:

adb shell am start -W -a android.intent.action.VIEW \
  -d "https://example.com/product/456" \
  -n com.example.myapp/.MainActivity

Verify that the app launches and processes the URL.

To test assetlinks.json, use the Android App Links Assistant tool, but the core mechanics are standard:

Common Pitfalls and Sharp Edges

Best Practices

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.