Integrating Deferred Deep Linking in Flutter Apps
Deferred deep linking allows mobile apps to route users to specific in-app content even if the app wasn’t installed when the link was clicked – closing the attribution gap. Instead of landing on a generic home screen after install, the user is taken exactly where they were headed.
This tutorial focuses on the technical implementation using Detour – a deferred deep linking tool build with developers in mind. We will focus on the technical implementation: getting the SDK running, handling the install ‘snapshot’, and passing custom data through app installation into your navigation.
Deep link vs. deferred deep link: what's the difference in Flutter
A deep link routes a user directly to a specific screen in your app – but only if the app is already installed. A deferred deep link does the same, but also survives the app installation process: if the app isn't installed yet, the link preserves the user's intended destination and context, routing them to the right screen on the very first open.
In practice, every deferred deep link is a deep link, just with an extra fallback path: install → first launch → match → route. That's the flow this guide walks through step by step, and it's also why iOS needs probabilistic matching (covered later) – unlike Android, there's no install referrer to rely on, so the SDK has to reconstruct the original link from device signals after the fact.
Configuring deferred deep links: Detour dashboard setup
As I’ve mentioned, for deferred deep linking to work, we need a middleman to persist information between the click and the app installation. This requires simple configuration in our dashboard.
First, sign up and create an organization and your application. If you have multiple environments, we suggest creating separate apps using a naming convention like <your-app>-staging and <your-app>-production.

iOS requirements
- Bundle ID: found in your Xcode project settings under the General tab, or in the Apple Developer Portal under “Identifiers.”
- Team ID: located in your Apple Developer Account under the “Membership” section.
- App Store ID: once your app is created in App Store Connect, you can find this string under “App Information” -> “Apple ID”.
Android requirements
- Package name: this is your
applicationId, found in your app-levelbuild.gradlefile. - Production certificate (SHA-256): since you’re likely using Google Play App Signing, you must grab this from the Google Play Console.
- Debug certificate (SHA-256): used for testing during local development. This is the one that matters when you’re running
flutter runon a device.

There is also an option to improve matching efficiency by using clipboard content. While not required, it serves as a common workaround for iOS privacy restrictions and helps boost accuracy in some scenarios.
Installing the deferred deep linking SDK for Flutter
This guide covers Flutter; if you’re on a different stack, we have companion guides for React Native and iOS.
detour_flutter_plugin is a thin bridge over the native Detour SDKs rather than a reimplementation, so the behaviour you get on each platform is the behaviour of the native SDK underneath. It requires Flutter >=3.3.0, Dart ^3.11.1, Android min SDK 24, and iOS 13.0+.
dependencies:
detour_flutter_plugin: ^1.2.0flutter pub getThe native dependencies come along automatically – com.swmansion.detour:detour-sdk:1.1.0 on Android, the Detour CocoaPod on iOS. Keep google() and mavenCentral() in your project repositories, and run pod install in ios/ after adding the package.
One thing worth calling out before you start sprinkling conditional imports through your code: on web, macOS, Linux, and Windows the plugin loads a no-op implementation. configure() does nothing, resolveInitialLink() returns a processed result with no link so your startup flow continues normally, and linkStream is empty. You can call the SDK unconditionally from shared code and multi-platform targets will still build and run.
Next, for the universal links part to work, paste the integration snippets generated in the dashboard into both native projects. On Android, into AndroidManifest.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" />
<data android:scheme="https" />
<data android:host="<your-host>" />
<data android:pathPrefix="/<your-app-hash>" />
</intent-filter>
On iOS, associated domains go into your Runner.entitlements:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:<your-host></string>
</array>
And if you want custom scheme links, register the scheme in Info.plist under CFBundleURLTypes and add a matching <data android:scheme="myapp" /> filter on Android.
There is one Flutter-specific trap on iOS. Plugins receive deep link callbacks through the Flutter delegate chain, so if you override the deep link methods in AppDelegate or SceneDelegate and don’t forward to super, Detour silently stops receiving runtime links:
override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
return super.application(app, open: url, options: options)
}
override func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
return super.application(
application,
continue: userActivity,
restorationHandler: restorationHandler
)
}
Finally, define your config:
const detourConfig = DetourConfig(
apiKey: '<REPLACE_WITH_YOUR_API_KEY>',
appID: '<REPLACE_WITH_APP_ID_FROM_PLATFORM>',
shouldUseClipboard: true,
linkProcessingMode: LinkProcessingMode.all,
);
linkProcessingMode decides which link sources the SDK claims. all is the default and handles universal/App Links, deferred links, and custom scheme links. webOnly skips custom schemes, and deferredOnly leaves every runtime link to your own routing layer and resolves deferred links only.
Integrating deferred deep links with Flutter navigation
The library handles the fingerprinting logic, but the biggest headache for developers has always been navigation integration. Auth gates, deep nesting, weird glitches, or the same deferred link triggering multiple times – are just a few of the common pain points that come to mind.
For the sake of this example, let’s simplify the structure: we’ll use an app with a sign-in screen, as well as a home screen and an onboarding screen – both of which are protected by an auth gate.
DetourService is the piece that makes this pleasant. It configures the native bridge once, merges the cold-start link and the runtime link stream into a single queue, exposes startup readiness through isInitialLinkProcessed, and – most importantly – uses explicit consume semantics: a resolved link sits in pendingIntent until you call consumePendingIntent(). That is what makes the auth gate almost trivial. If the app is not ready to show the destination yet, you simply don’t consume, and the link waits.
class _AppState extends State<App> {
final _detour = DetourService();
@override
void initState() {
super.initState();
// Re-check on both sources: a link can arrive before sign-in,
// and sign-in can complete long after the link resolved.
_detour.addListener(_routeIfReady);
auth.addListener(_routeIfReady);
_start();
}
Future<void> _start() => _detour.start(detourConfig);
void _routeIfReady() {
if (!_detour.isInitialLinkProcessed || !auth.isLoaded) return;
final intent = _detour.pendingIntent;
if (intent == null) return;
// Not ready yet — leave the intent unconsumed and it will still be
// here once sign-in and onboarding are done.
if (!auth.isSignedIn || !auth.isOnboardingCompleted) return;
navigatorKey.currentState?.pushNamed(intent.link.route);
_detour.consumePendingIntent();
}
@override
void dispose() {
_detour.removeListener(_routeIfReady);
auth.removeListener(_routeIfReady);
_detour.dispose();
super.dispose();
}
}
Use isInitialLinkProcessed to keep your splash screen up until the SDK has decided whether there is a link – that is what stops the home screen from flashing before a deferred link lands.
intent.link.route is the path plus query string. link.pathname is the path alone, link.params the query already parsed into a Map<String, String>, link.url the raw string, and link.type tells you whether the link arrived as deferred, verified, or scheme – so a single branch of routing code covers deferred installs, App Links, and scheme links alike.
For the low-level DetourFlutterPlugin API, manual processLink calls, and duplicate-emission handling, see the plugin usage docs.
Customizing Flutter deep link parameters and short links
By default, every application in our dashboard has a link assigned to it, to which you can append any URL parameters you want:
https://your-org.godetour.link/<app-id>
For example, if you wanted to redirect to a product screen, you would just append this path:
https://your-org.godetour.link/<app-id>/products/42
And if you wanted to let’s say track a marketing campaign, you would add the UTM properties like that:
https://your-org.godetour.link/<app-id>/products/42?utm_source=SP&utm_origin=web&utm_screen=header
Everything after the app hash lands in link.route, and the query string is parsed into link.params for you.
Keeping the link clean
While this manual approach is highly flexible, adding too many parameters can result in long, unwieldy URLs that aren’t ideal for public-facing content.
The solution is to create a short link within our management panel. This allows you to predefine your parameter list and condense the entire string into a clean, professional-looking hash that is much easier to share.
Testing deferred deep links in Flutter
Verifying this flow is straightforward, though there are a few platform-specific nuances to keep in mind:
- Store presence: your application must be available on the App Store or Google Play Store to successfully complete the required redirect chain.
- Android referrer API: while simulating this API often requires specific workarounds, you can easily verify the flow using probabilistic matching as a simpler alternative.
- First launch only: deferred matching runs on the first launch after install, so reinstall the app between test runs.
With all these considerations in place, you can just test your link by opening it in your browser, or by scanning the QR code from our panel.
For ADB and simulator commands plus a troubleshooting checklist, see the testing docs.
Handling web fallback for Flutter deep links
While this process is optimized for mobile apps, you may wonder what happens when a user clicks a link on a desktop browser. By default, users are redirected to the App Store or Google Play; however, if you have a relevant website, you can configure it as a fallback URL. Furthermore, if your links include specific attribution parameters – such as a chat invitation ID – you can choose to pass these through to your web platform and handle everything in there as well.
Improving deep link matching accuracy and time window
In some cases, the matching score might not be high enough for the process to succeed. While the default value is 850, you can always adjust this in our panel if there is a high probability that certain attribution parameters might not be captured properly.
The same flexibility applies to the matching time window – the duration between the initial user click and the matching process once the app download is complete. By default, this is set to 15 minutes, which is typically sufficient for a standard installation, but the setting is customizable to fit your specific case.
Have any questions? The Detour team is here to help
We’re building Detour specifically for developers, which means your feedback is our roadmap. If you have questions or feature ideas, please reach out to us:
- On our Discord
- Directly through our email – contact@godetour.dev
And if there are any technical details you are interested in, check out our documentation and examples.


