Flutter Deferred Deep Links
Preserve a link's destination through App Store and Play Store installs, then route the user on first launch, in Flutter.
Deferred deep links let you route users to specific content even when they don't have your app installed. The user clicks a link, installs from the App Store or Play Store, and on first launch the SDK matches them back to the original link.
How It Works
- User taps a WarpLink URL in a browser
- WarpLink records the click. The server derives the IP and stores it with the normalized language and timezone. Clicks that share a fingerprint are kept as a list, newest first, up to 10, so a second click never overwrites the first
- User is redirected to the App Store (iOS) or Play Store (Android)
- User installs and opens the app
- SDK detects first launch. A completion marker is written only after the check completes. It is scoped to one install on both platforms, and no backup restores it, so a reinstall attributes fresh. On iOS, if the backup-excluded file cannot be written, the SDK falls back to a UserDefaults flag that a restore does carry; that rare case is the one exception
- SDK collects device signals (preferred language, timezone name and offset, and platform IDs) and sends them to the attribution API. The server derives the IP and computes the fingerprint
- Server matches against stored click data
- Deep link returned with
isDeferred: true
Automatic Check
The deferred check fires automatically from configure(). When the install also came from a link tap, the check starts after that link is delivered or dropped. The result arrives in your onLink callback, where isDeferred is true:
import 'package:warplink_flutter/warplink_flutter.dart';
// Call from your root widget's initState
WarpLink.configure(
apiKey: 'wl_live_yoursdkkeyhere000000000000000000',
onLink: (event) {
if (event is! LinkEvent) return;
final deepLink = event.deepLink;
if (deepLink.isDeferred) {
final confidence = deepLink.matchConfidence ?? 0;
if (confidence > 0.5) navigateTo(deepLink.deepLinkUrl ?? deepLink.destination);
} else {
navigateTo(deepLink.destination); // a tap, not a deferred install
}
},
);
If that check cannot reach the server, for example on a launch with no network, the failure also arrives in onLink. The first-launch gate stays open, so the next launch retries by itself. Treat a WarpLinkNetworkException there as a report, not as something to act on.
Manual Check (Advanced)
To run the check yourself, set automaticDeferredDeepLinks: false and call checkDeferredDeepLink():
import 'package:flutter/material.dart';
import 'package:warplink_flutter/warplink_flutter.dart';
class _AppState extends State<App> {
@override
void initState() {
super.initState();
_checkDeferred();
}
Future<void> _checkDeferred() async {
try {
await WarpLink.configure(
apiKey: 'wl_live_yoursdkkeyhere000000000000000000',
automaticDeferredDeepLinks: false,
);
final link = await WarpLink.checkDeferredDeepLink();
if (link != null && link.isDeferred) {
navigateTo(link.deepLinkUrl ?? link.destination);
}
} on WarpLinkException catch (error) {
debugPrint('Deferred deep link check failed: ${error.message}');
}
}
}
Confidence-Based Routing
final link = await WarpLink.checkDeferredDeepLink();
if (link == null || !link.isDeferred) return;
final confidence = link.matchConfidence ?? 0;
if (confidence > 0.5) {
// High confidence: navigate straight to the link's destination
navigateTo(link.deepLinkUrl ?? link.destination);
} else if (confidence > 0.3) {
// Medium confidence: show suggestion
showSuggestion(
message: 'Were you looking for this?',
url: link.destination,
);
}
// Below 0.3: ignore, show default onboarding
Confidence Scores
Probabilistic scores depend on the fingerprint variant. enriched_tz hashes the IANA timezone name and is what an SDK that collects a zone name sends. enriched hashes the minute offset instead and stays as the fallback for older SDKs. basic drops the timezone entirely.
| Scenario | Confidence | Match Type |
|---|---|---|
| IDFV match, same install asking again (iOS, not a reinstall) | 1.0 | deterministic |
| Play Install Referrer (Android) | 1.0 | deterministic |
enriched_tz fingerprint, < 1 hour | 0.85 | probabilistic |
enriched_tz fingerprint, < 3 hours | 0.65 | probabilistic |
enriched_tz fingerprint, < 6 hours | 0.50 | probabilistic |
enriched_tz fingerprint, < 24 hours | 0.30 | probabilistic |
The offset variant scores 0.80 / 0.60 / 0.45 / 0.25 across the same bands, and basic scores 0.70 / 0.50 / 0.35 / 0.20. Two further signals can only reduce the score: more than one distinct link in the fingerprint bucket applies x0.6, and a shared click IP applies x0.6 for carrier-grade NAT or a private address, x0.9 for a household IPv4 address, and x1.0 for IPv6. Past 24 hours there is no match at all.
Both deterministic matches set matchGuaranteed to true on the returned link. Use that flag, like matchConfidence, to pick a destination, not as a credential. A probabilistic match is a best guess made from a network-shaped fingerprint and can name the wrong user, so it should only pick a low-risk destination with a safe fallback; authenticate the user and check authorization separately before anything sensitive, such as signing in or showing personal data.
Match Window
The match window is set per link on the server, not in the SDK. Configure it in the dashboard when you create or edit a link. The default is 6 hours and the ceiling is 24 hours, so the 24 hour band above only applies to links configured past the default.
The window is deliberately short. The fingerprint key is a network (IP, language, timezone), not a device, so every extra hour lets another stranger behind the same shared address join the bucket while adding almost no real matches. Links created before the current limits may still carry a longer stored value, but the server caps every window at 24 hours when it reads them.
This governs probabilistic matching only. The IDFV and Play Install Referrer branches are deterministic and are not affected by the window.
Platform Differences
| iOS | Android | |
|---|---|---|
| Deterministic match | IDFV (same install asking again, not a reinstall) | Play Install Referrer |
| Completion marker | Backup-excluded file in the app container | File in noBackupFilesDir |
| Device-seen marker | Keychain | SharedPreferences (restored by Auto Backup) |
| ATT required? | No (IDFV is exempt) | N/A |
The completion marker is per install and no backup restores it, so a reinstall re-attributes on both platforms. The device-seen marker outlives an uninstall on both platforms and only sets is_reinstall on the request. The one exception is the iOS fallback described above.
Caching Behavior
- Attribution check happens once per install (first launch only)
- Result stored by the native SDK
- Subsequent calls to
checkDeferredDeepLink()resolve with the stored result, the matched deep link if there was one andnullif there was not, without another network request - A reinstall is a new install, so it gets a new check. See App Reinstall
Edge Cases
Offline First Launch
checkDeferredDeepLink() throws a WarpLinkNetworkException (E_NETWORK_ERROR). The attempt is not consumed, so the SDK retries the check on the next launch.
Check for connectivity before calling checkDeferredDeepLink() if your first-launch experience depends on it.
App Reinstall
A reinstall is a new install and is attributed again on both platforms. The Dart layer does nothing special here: the plugin calls the native SDK, which keeps two markers with two different jobs.
- Completion marker. Says the check already ran for this install, and it is gone once the app is. iOS keeps it in a backup-excluded file in the app container, Android in
noBackupFilesDir. Neither comes back from a restore, so a reinstall starts clean. The one exception is the iOS fallback described above. - Device-seen marker. Says the device was attributed at some point, and it outlives an uninstall. iOS keeps it in the Keychain, Android in
SharedPreferences, which Auto Backup restores. It gates nothing. Its only job is to setis_reinstall: trueon the attribution request
Both installs count. The dashboard shows installs and reinstalls together as one installs number.
Multiple Links Before Install
Clicks that share a fingerprint are kept as a list, newest first, up to 10. The most recent click your app can claim is the one matched, and the others stay available for the other devices that share the address. When more than one click is claimable, the confidence score is multiplied by 0.6 to report that ambiguity.
Testing
Deleting or uninstalling the app is enough to retest. The completion marker goes with it, so the next install runs the check again:
- Delete (iOS) or uninstall (Android) the app from the test device
- Open the test link in Safari or Chrome. You're redirected to the App Store, Play Store, or fallback URL
- Install the app via Xcode/TestFlight or Android Studio/
adb install - Launch the app. The matched deep link should arrive in
onLinkwithisDeferredset totrue
The request from step 4 carries is_reinstall: true, because the device-seen marker from the earlier install is still on the device. To test the first-install path instead, use a device or simulator/emulator that has never run the app, or erase it first: on iOS Simulator, Device > Erase All Content and Settings (or xcrun simctl erase); on Android, clear app data along with an uninstall, since SharedPreferences on Android is restored by Auto Backup.
Flutter Deep Links
Handle deep links in your Flutter app across iOS and Android, covering both cold start and warm start with the WarpLink SDK.
Flutter Install Attribution
How the WarpLink Flutter SDK attributes installs across iOS and Android, with match types, confidence scores, and privacy controls.