Learn how to use Dub to create deep links for your mobile app (with native support for React Native, iOS, and Android).
Deep links require a Pro plan subscription or
higher.
On Dub, you can create deep links that lets you to redirect users to a specific page within your mobile application.
For example, you’re creating an ad campaign to drive traffic to a specific product page within your mobile app.By following the quickstart guide below, you’ll be able to make sure that all your short links are set up with deep linking functionality.
Before you can create deep links, you need to configure your deep link domains in your Dub workspace. This involves adding a custom domain that will be used for your deep links and configuring your deep link configuration files.
First, you’ll need to add a custom domain to your Dub workspace. Navigate to your workspace domain settings and click Add Domain.You can use a domain you already own, or leverage our free .link domain offer to register a custom domain like yourcompany.link and use it as your deep link domain.
Once you’ve set up your custom domain, you’ll need to upload your deep link configuration files to Dub, which we’ll host under the /.well-known/ directory of your domain.To do that, go to your workspace domain settings and click on the edit button for your custom domain:
In the domain modal, click on Show advanced settings, which will open up the Deep Link Configuration settings fields.
After updating the AASA file, you may need to reinstall your app since iOS can
cache the old version of the file, which can lead to inconsistent behavior.
Android (assetlinks.json)For Android apps, upload your AssetLinks file to enable Android deep links:
Verify that your configuration files are set up correctly
Once you’ve set up your deep link configuration files, you can go to their respective URLs to verify that they’ve been configured correctly:iOS:yourdomain.link/.well-known/apple-app-site-association(example)Android:yourdomain.link/.well-known/assetlinks.json(example)
Last but not least, you’ll need to allowlist your deep link domain in your apps to allow them to redirect straight into a page within your app.For iOS apps, you’ll need to allow websites to link to your apps.For Android apps, you’ll need to verify your Android app links.
Go to your Dub dashboard and click Create Link in the top navigation bar.Enter your destination URL in the “Destination URL” field. You can enter the URL with or without the https:// protocol – behind the scenes, Dub will automatically make sure it’s formatted correctly.This is the URL that opens a specific screen or piece of content within your app. For example https://yourapp.com/product/Apple-MacBook opens the product detail screen for Apple-MacBook.
Device Targeting enables you to redirect users to the App Store or Google Play Store if your app isn’t installed.For example, you can set custom destination URLs for different devices using the link builder — use the iOS Targeting input for iOS devices, and the Android Targeting input for Android devices.
Finally, click Create link to create your deep link. This link will act as a Firebase Dynamic Link replacement.
Dub’s link builder offers many additional features like UTM builder, password
protection, expiration dates, geo targeting, and more. Learn more about
creating links on Dub to explore all
available options.
When your app is already installed, the deep link will open your app directly.
You may handle the deep link manually or with the supported mobile SDKs.Option 1: Handle the deep link using a supported Dub Mobile SDK (iOS & React Native only)Follow our installation guide for Swift or React Native to get started.
import { useState, useEffect, useRef } from "react";import { Linking } from "react-native";import AsyncStorage from "@react-native-async-storage/async-storage";import dub from "@dub/react-native";export default function App() { useEffect(() => { dub.init({ publishableKey: "<DUB_PUBLISHABLE_KEY>", domain: "<DUB_DOMAIN>", }); // Check if this is first launch const isFirstLaunch = await AsyncStorage.getItem("is_first_launch"); if (isFirstLaunch === null) { await handleFirstLaunch(); await AsyncStorage.setItem("is_first_launch", "false"); } else { // Handle initial deep link url (Android only) const url = await Linking.getInitialURL(); if (url) { await handleDeepLink(url); } } const linkingListener = Linking.addEventListener("url", (event) => { handleDeepLink(event.url); }); return () => { linkingListener.remove(); }; }, []); const handleFirstLaunch = async ( deepLinkUrl?: string | null | undefined, ): Promise<void> => { try { const response = await dub.trackOpen(deepLinkUrl); const destinationURL = response.link?.url; // Navigate to the destination URL } catch (error) { // Handle error } }; // Return your app...}
// ContentView.swiftimport SwiftUIimport Dubstruct ContentView: View { @Environment(\.dub) var dub: Dub @AppStorage("is_first_launch") private var isFirstLaunch = true var body: some View { NavigationStack { VStack { // Your app content } .onOpenURL { url in trackOpen(deepLink: url) } .onAppear { if isFirstLaunch { trackOpen() isFirstLaunch = false } } } } private func trackOpen(deepLink: URL? = nil) { Task { do { let response = try await dub.trackOpen(deepLink: deepLink) // Obtain the destination URL from the response guard let url = response.link?.url else { return } // Navigate to the destination URL } catch let error as DubError { print(error.localizedDescription) } } }}
import UIKitimport Dub@mainclass AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? private let dubPublishableKey = "<DUB_PUBLISHABLE_KEY>" private let dubDomain = "<DUB_DOMAIN>" private var isFirstLaunch: Bool { get { UserDefaults.standard.object(forKey: "is_first_launch") as? Bool ?? true } set { UserDefaults.standard.set(newValue, forKey: "is_first_launch") } } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Dub.setup(publishableKey: dubPublishableKey, domain: dubDomain) // Track first launch if isFirstLaunch { trackOpen() isFirstLaunch = false } // Override point for customization after application launch. return true } func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { handleDeepLink(url: url) return true } func handleDeepLink(url: URL) { trackOpen(deepLink: url) } private func trackOpen(deepLink: URL? = nil) { // Call the tracking endpoint with the full deep link URL Task { do { let response = try await Dub.shared.trackOpen(deepLink: deepLink) print(response) // Navigate to final link via link.url guard let destinationUrl = response.link?.url else { return } // Navigate to the destination URL } catch let error as DubError { print(error.localizedDescription) } } }}
Option 2: Handle the deep link manually
1
Detect the deep link
Your app will receive the deep link URL when it opens. The URL will be in the format: https://yourdomain.link/short-link-slug
// App.jsimport { useEffect } from "react";import { Linking } from "react-native";useEffect(() => { const handleDeepLink = (url) => { try { // Call the tracking endpoint with the full deep link URL trackDeepLinkClick(url); } catch (error) { console.error("Error handling deep link URL:", error); } }; // Handle deep link when app is already running const subscription = Linking.addEventListener("url", (event) => { handleDeepLink(event.url); }); // Handle deep link when app is opened from a deep link Linking.getInitialURL().then((url) => { if (url) { handleDeepLink(url); } }); return () => { subscription?.remove(); };}, []);
// AppDelegate.swiftfunc application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { handleDeepLink(url: url) return true}func handleDeepLink(url: URL) { // Call the tracking endpoint with the full deep link URL trackDeepLinkClick(deepLink: url.absoluteString)}
// ContentView.swiftstruct ContentView: View { var body: some View { NavigationStack { VStack { //... your app content } .onOpenURL { url in // Call the tracking endpoint with the full deep link URL trackDeepLinkClick(deepLink: url.absoluteString) } } }}
// MainActivity.ktoverride fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) intent?.data?.let { uri -> handleDeepLink(uri) }}private fun handleDeepLink(uri: Uri) { // Call the tracking endpoint with the full deep link URL trackDeepLinkClick(uri.toString())}
2
Track the deep link open
Once you’ve detected and parsed the deep link, you should track the open event using Dub’s API, which will return the final destination URL in the API response.To do this, make a POST request to the /track/open endpoint with the following body:
Now you’ve got the destination URL (via link.url), you can navigate the user to the correct screen in your app.Here’s the full example code for React Native, iOS, and Android.
// DeepLinkTracker.jsasync function trackDeepLinkClick(deepLink) { try { const response = await fetch(`https://api.dub.co/track/open`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ deepLink, }), }); if (response.ok) { const data = await response.json(); const destinationUrl = data.link.url; // Navigate to the destination URL in your app navigateToDestination(destinationUrl); } } catch (error) { console.error("Error tracking deep link:", error); }}
// DeepLinkTracker.swiftfunc trackDeepLinkClick(deepLink: String) { let url = URL(string: "https://api.dub.co/track/open")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let body = [ "deepLink": deepLink, ] request.httpBody = try? JSONSerialization.data(withJSONObject: body) URLSession.shared.dataTask(with: request) { data, response, error in if let data = data, let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let link = json["link"] as? [String: Any], let destinationUrl = link["url"] as? String { DispatchQueue.main.async { self.navigateToDestination(destinationUrl) } } }.resume()}
// DeepLinkTracker.ktprivate fun trackDeepLinkClick(deepLink: String) { val url = URL("https://api.dub.co/track/open") val connection = url.openConnection() as HttpURLConnection connection.requestMethod = "POST" connection.setRequestProperty("Content-Type", "application/json") connection.doOutput = true val body = JSONObject().apply { put("deepLink", deepLink) } connection.outputStream.use { os -> os.write(body.toString().toByteArray()) } val response = connection.inputStream.bufferedReader().use { it.readText() } val jsonResponse = JSONObject(response) val link = jsonResponse.getJSONObject("link") val destinationUrl = link.getString("url") runOnUiThread { navigateToDestination(destinationUrl) }}
When your app isn’t installed, the user will be redirected to the App Store or Google Play Store since you’ve enabled device targeting in Step 2 above.After they install and open your app, you’ll need to use deferred deep linking to:
Retrieve the short link that brought the user to the app store
For detailed implementation of deferred deep linking, including how to use the
Google Play Store Install Referrer API and other services, see our Deferred
Deep Linking guide.
If your deep links aren’t working as expected, here are some common issues and solutions:
1
Deep link domain not allowlisted
Make sure you’ve allowlisted your deep link domain in your app’s configuration. This is required for both iOS and Android to recognize and handle links from your domain.
2
iOS/Android cached outdated deep link configuration
If you’ve completed all steps above and your deep links still aren’t working, it’s likely because iOS or Android cached the outdated deep link configuration.Try uninstalling and reinstalling your app to clear the cache. This forces the operating system to re-fetch the apple-app-site-association or assetlinks.json files.
3
Deep link wrapped by a third-party links
If your links are being shared through marketing platforms, email providers, or click trackers, the original URL may be getting wrapped or modified.When a third-party service wraps your Dub link in their own tracking URL, the deep link won’t work because the device sees the wrapper domain instead of your allowlisted domain.Contact your marketing or analytics provider to ensure Dub links are passed through without modification.
4
iOS URL handler short-circuited by third-party SDKs
If you’re using third-party SDKs (e.g., Facebook/Meta SDK, Google Sign-In, or other authentication SDKs) alongside Dub in an iOS app, your openURL handler may silently fail to trigger Dub’s deep link tracking.This happens when multiple URL handlers are chained with a short-circuit || (OR) operator in your AppDelegate. If a third-party SDK processes the URL first and returns true, subsequent handlers never execute — meaning RCTLinkingManager (React Native) or Dub’s handleDeepLink / trackOpen never gets called.This is especially common when links are opened from in-app browsers (e.g., Facebook or Instagram’s in-app browser).How to fix this:Replace the short-circuit || with a non-short-circuit | operator (or call all handlers independently) to ensure every SDK gets a chance to process the URL:
// ❌ Bad: Short-circuit OR — if the Facebook SDK returns true,// handleDeepLink never firesfunc application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { return ApplicationDelegate.shared.application(app, open: url, options: options) || handleDeepLink(url: url)}// ✅ Good: Call each handler independentlyfunc application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { let fbHandled = ApplicationDelegate.shared.application(app, open: url, options: options) let dubHandled = handleDeepLink(url: url) return fbHandled || dubHandled}
This issue is easy to miss because deep links may still work in most
scenarios — it only fails when the URL is first processed by another SDK
(e.g., when a user opens your link from Facebook or Instagram’s in-app
browser). Always test deep links from third-party apps where your SDKs are
active.