Deferred Deep Links (DDLs) provide the most seamless way to guide users from your funnel to your mobile application after purchase, eliminating the need for manual login or authentication.
In this guide we’ll tell you how to configure deep links with AppsFlyer or Adjust.
You’ll need an active AppsFlyer or Adjust account to implement deep links.
Deep links
Deferred deep links (DDLs) enable you to pass information (such as user identifiers or tokens) through a single link that can:
- Redirect new users to the App Store if the app isn’t installed.
- Automatically open the app if it’s already installed.
- Deliver metadata (like authentication tokens) into the app on first launch, even after installation.
This creates a frictionless post-purchase experience with no login steps required.
Prerequisites
Before setting up deep links, ensure you have:
- AppsFlyer account with OneLink configured, or an Adjust account.
- Mobile app with the AppsFlyer or Adjust SDK integrated.
- FunnelFox funnel ready for button configuration.
Configuration
Setting up deferred deep links requires configuring both your funnel and mobile app to pass and receive user identification data.
1. Funnel setup
Generate AppsFlyer OneLink
- Log into your AppsFlyer dashboard.
- Create a new OneLink or use an existing one.
- Copy your OneLink URL (format:
https://yourapp.onelink.me/example).
Create button element
- In your funnel, add a Button element where you want the app redirect.
- Configure the button with External Link action.
- (Optional) Mark this button as CTA for analytics tracking
Configure deep link URL
Set the button link with ffkey query parameter using user variables:https://yourapp.onelink.me/example?ffkey={{user.id}}
You can use other identifiers like {{user.session_id}} or custom properties depending on your authentication flow.
Create custom link in Adjust
- Go to Campaign Lab > Custom links in your Adjust dashboard.
- Create a new custom link.
- Create a new deep link under the Link URLs tab of your custom link.
- Define the deep link routes under the User destinations tab and specify redirects for users who have not installed your app.
- Add a query parameter to carry the user ID, for example:
https://example.go.link?adj_t=abc123&user_id={user_id}.
- Generate the deep link.
Create button element
- In your funnel, add a Button element where you want the app redirect.
- Configure the button with External Link action.
- (Optional) Mark this button as CTA for analytics tracking.
Configure deep link URL
Set the button link to include the query parameter you’ve added for the user ID using user variables:https://example.go.link?adj_t=abc123&user_id={{user.id}}
You can use other identifiers like {{user.session_id}} or custom properties depending on your authentication flow.
Your web configuration is now complete! Next, you’ll need to handle these links in your mobile application.
2. Mobile app setup
The goal is to get the user ID into the app and use it for identification (e.g., with Adapty).
When the app isn’t installed, AppsFlyer SDK will invoke a callback (typically onConversionDataSuccess) with the original link parameters after installation.Set up your mobile app in three steps:
- Listen for the callback from AppsFlyer SDK.
- Extract
ffkey from the conversion data.
- Identify/authenticate the user using the extracted key.
For complete implementation details, refer to the AppsFlyer documentation.Code examples: Swift
Kotlin
React Native
import AppsFlyerLib
class AppsFlyerDelegateHandler: NSObject, AppsFlyerLibDelegate {
static let shared = AppsFlyerDelegateHandler()
func onConversionDataSuccess(_ data: [AnyHashable : Any]) {
guard let status = data["af_status"] as? String,
status == "Non-organic" else { return }
if let sessionId = data["ffkey"] as? String {
// Identify user using the AppsFlyer DDL ffkey from conversion data.
}
}
func onConversionDataFail(_ error: Error) {
print("AppsFlyer DDL failed: \(error)")
}
}
object AppsFlyerHandler : AppsFlyerConversionListener {
override fun onConversionDataSuccess(data: Map<String, Any>?) {
if (data == null) return
val status = data["af_status"] as? String
if (status == "Non-organic") {
val sessionId = data["ffkey"] as? String
if (sessionId != null) {
// Identify user using the AppsFlyer DDL ffkey from conversion data.
}
}
}
override fun onConversionDataFail(errorMessage: String?) {
Log.e("AppsFlyer", "AppsFlyer DDL failed: $errorMessage")
}
override fun onAppOpenAttribution(data: Map<String, String>?) {}
override fun onAttributionFailure(errorMessage: String?) {}
}
appsFlyer.onInstallConversionData((res) => {
const data = res.data;
if (data.af_status === 'Non-organic' && data.ffkey) {
// Identify user using the AppsFlyer DDL ffkey from conversion data.
}
});
Since your Adjust tracker link appends user_id={{user.id}}, the SDK will return a deferred deep link URL that includes user_id. Parse it in the callback and identify the user.Set up your mobile app and backend in three steps:
- Enable the deferred deep link callback in the Adjust SDK.
- Parse the
user_id query parameter from the returned URL.
- Identify/authenticate the user using
user_id (e.g., with Adapty or your backend).
For complete implementation details, refer to the Adjust documentation.Code examples: Swift
Kotlin
React Native
import AdjustSdk
import Foundation
// During setup:
// let config = ADJConfig(appToken: "...", environment: ADJEnvironmentProduction)
// config?.delegate = AdjustDelegateHandler.shared
// Adjust.initSdk(config)
final class AdjustDelegateHandler: NSObject, AdjustDelegate {
// Deferred deep link URL (e.g., ...?user_id=...)
func adjustDeferredDeeplinkReceived(_ deeplink: URL?) -> Bool {
guard
let url = deeplink,
let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems,
let userId = items.first(where: { $0.name == "user_id" })?.value
else { return false }
// Identify user with userId
return false // true = auto-open; false = you handle routing
}
import android.net.Uri
import com.adjust.sdk.AdjustConfig
// Hook during setup:
// val config = AdjustConfig(context, "YOUR_APP_TOKEN", AdjustConfig.ENVIRONMENT_PRODUCTION)
// AdjustDeeplinkHandler.attach(config)
// Adjust.initSdk(config)
object AdjustDeeplinkHandler {
fun attach(config: AdjustConfig) {
// Inspect deferred deep link (includes ?user_id=...)
config.setOnDeferredDeeplinkResponseListener { deeplink: Uri ->
deeplink.getQueryParameter("user_id")?.let { userId ->
// Identify user with userId
}
false // true = auto-open; false = you handle routing
}
}
}
import { Adjust, AdjustConfig } from 'react-native-adjust';
// During setup:
const config = new AdjustConfig('YOUR_APP_TOKEN', AdjustConfig.EnvironmentProduction);
// Optional: config.disableDeferredDeeplinkOpening();
config.setDeferredDeeplinkCallback((deeplink) => {
try {
const u = new URL(deeplink.deeplink); // string URL from Adjust
const userId = u.searchParams.get('user_id');
if (userId) {
// Identify user with userId
}
} catch {}
});
Adjust.initSdk(config);