> ## Documentation Index
> Fetch the complete documentation index at: https://funnelfox.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Deep links for authentication

> Set up deferred deep links in FunnelFox for seamless web-to-app user authentication. Pass tokens and user IDs after purchase.

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.

You can use any DDL provider of your choice. As an example, this guide explains how to configure deep links with Adapty UA, AppsFlyer, or Adjust.

<Note>
  You'll need an active Adapty UA, AppsFlyer, or Adjust account to implement deep links.
</Note>

## 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.

<Tip>The customer email is saved in the `{{email}}` variable (from Email input or OAuth), so you can include it in your deep links.</Tip>

## Prerequisites

Before setting up deep links, ensure you have:

* Adapty UA tracking link configured, an AppsFlyer account with OneLink configured, or an Adjust account. You can also use any other DDL provider of your choice.
* Mobile app with the Adapty, AppsFlyer, or Adjust SDK integrated.
* FunnelFox funnel ready for [button configuration](/elements/button).

## Configuration

Setting up deferred deep links requires configuring both your funnel and mobile app to pass and receive user identification data.

### 1. Funnel setup

<Tabs>
  <Tab title="Adapty UA">
    <Steps>
      <Step title="Create iOS and Android links in Adapty UA">
        1. Open [Adapty UA](https://app.adapty.io/integrations/user-acquisition) in the Adapty dashboard.
        2. Create an **iOS** tracking link and an **Android** tracking link.
        3. Append a deferred data parameter to each link to carry the user ID using [user variables](/editor/variables#system-variables):

        * For the iOS link, add `&ios_deferred_data={{user.id}}`.
        * For the Android link, add `&android_deferred_data={{user.id}}`.

        4. Copy the click links you'll use in the funnel. Example URLs:

        ```
        https://api-ua.adapty.io/api/v1/attribution/click?adpt_cid=__ADAPTY_ID__&ios_deferred_data={{user.id}}&redirect_url=__APP_LINK__
        ```

        ```
        https://api-ua.adapty.io/api/v1/attribution/click?adpt_cid=__ADAPTY_ID__&android_deferred_data={{user.id}}&redirect_url=__APP_LINK__
        ```

        <Info>Learn about [deferred deeplinks in Adapty UA](https://adapty.io/docs/ua-deferred-data).</Info>
      </Step>

      <Step title="Add button elements with conditional visibility">
        Adapty uses a different deferred data parameter for each platform, so your funnel needs one button per platform.

        1. Add two **Button** elements where you want the app redirect (one for iOS, one for Android).
        2. Use [conditional visibility](/elements/overview#conditional-visibility) on each button with a **Device OS** condition: show the first button only on iOS and the second only on Android.
        3. Configure each button with an **External Link** [action](/editor/actions).
      </Step>

      <Step title="Set the deep link URLs on the buttons">
        Paste the Adapty UA links from step 1 into the buttons:

        * Set the **iOS button** link to the iOS Adapty URL containing `ios_deferred_data={{user.id}}`.
        * Set the **Android button** link to the Android Adapty URL containing `android_deferred_data={{user.id}}`.

        <Info>
          You can use other identifiers like `{{user.session_id}}` or custom properties depending on your authentication flow.
        </Info>
      </Step>
    </Steps>
  </Tab>

  <Tab title="AppsFlyer">
    <Steps>
      <Step title="Generate AppsFlyer OneLink">
        1. Log into your [AppsFlyer dashboard](https://hq1.appsflyer.com/onelink).
        2. Create a new OneLink or use an existing one.
        3. Copy your OneLink URL (format: `https://yourapp.onelink.me/example`).

        <Info>Learn about [creating deep links in AppsFlyer](https://support.appsflyer.com/hc/en-us/articles/208874366-Create-a-OneLink-link-for-your-campaigns).</Info>
      </Step>

      <Step title="Create button element">
        1. In your funnel, add a **Button** element where you want the app redirect.
        2. Configure the [button](/elements/button) with **External Link** [action](/editor/actions).
        3. (Optional) Mark this button as **CTA** for analytics tracking
      </Step>

      <Step title="Configure deep link URL">
        Set the button link with `ffkey` query parameter using [user variables](/editor/variables#system-variables):

        ```
        https://yourapp.onelink.me/example?ffkey={{user.id}}
        ```

        <Info>
          You can use other identifiers like `{{user.session_id}}` or custom properties depending on your authentication flow.
        </Info>
      </Step>
    </Steps>
  </Tab>

  <Tab title="Adjust">
    <Steps>
      <Step title="Create custom link in Adjust">
        1. Go to **Campaign Lab > Custom links** in your [Adjust dashboard](https://suite.adjust.com/).
        2. Create a new custom link.
        3. Create a new deep link under the **Link URLs** tab of your custom link.
        4. Define the deep link routes under the **User destinations** tab and specify redirects for users who have not installed your app.
        5. Add a query parameter to carry the user ID, for example: `https://example.go.link?adj_t=abc123&user_id={user_id}`.
        6. Generate the deep link.

        <Info>Learn about [creating deep links in Adjust](https://help.adjust.com/en/article/deep-links).</Info>
      </Step>

      <Step title="Create button element">
        1. In your funnel, add a **Button** element where you want the app redirect.
        2. Configure the [button](/elements/button) with **External Link** [action](/editor/actions).
        3. (Optional) Mark this button as **CTA** for analytics tracking.
      </Step>

      <Step title="Configure deep link URL">
        Set the button link to include the query parameter you've added for the user ID using [user variables](/editor/variables#system-variables):

        ```
        https://example.go.link?adj_t=abc123&user_id={{user.id}}
        ```

        <Info>
          You can use other identifiers like `{{user.session_id}}` or custom properties depending on your authentication flow.
        </Info>
      </Step>
    </Steps>
  </Tab>
</Tabs>

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).

<Tabs>
  <Tab title="Adapty UA">
    When a user clicks an Adapty UA link, Adapty saves the click data (including any `ios_deferred_data` or `android_deferred_data` value you appended).

    After the user installs the app and opens it for the first time, Adapty matches the install to the click and delivers the payload to the SDK through the `onInstallationDetailsSuccess` callback.

    Set up your mobile app in three steps:

    1. Implement the `onInstallationDetailsSuccess` callback on the Adapty SDK.
    2. Parse `details.payload` (an escaped JSON string) and read `ios_deferred_data` on iOS or `android_deferred_data` on Android.
    3. Identify/authenticate the user using the extracted value.

    For complete implementation details, refer to the [Adapty UA deferred deeplinks documentation](https://adapty.io/docs/ua-deferred-data).

    **Code examples**:

    <Tabs>
      <Tab title="Swift">
        ```swift theme={null}
        import Adapty

        // During setup:
        //   Adapty.delegate = AdaptyDelegateHandler.shared

        final class AdaptyDelegateHandler: NSObject, AdaptyDelegate {
            static let shared = AdaptyDelegateHandler()

            nonisolated func onInstallationDetailsSuccess(_ details: AdaptyInstallationDetails) {
                guard
                    let payloadStr = details.payload,
                    let data = payloadStr.data(using: .utf8),
                    let payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
                    let userId = payload["ios_deferred_data"] as? String
                else { return }

                // Identify user with userId
            }
        }
        ```
      </Tab>

      <Tab title="Kotlin">
        ```kotlin theme={null}
        Adapty.setOnInstallationDetailsListener(object : OnInstallationDetailsListener {
            override fun onInstallationDetailsSuccess(details: AdaptyInstallationDetails) {
                details.payload?.let {
                    runCatching {
                        val json = JSONObject(it)
                        val userId = json.optString("android_deferred_data")
                        if (userId.isNotEmpty()) {
                            // Identify user with userId
                        }
                    }.onFailure(Throwable::printStackTrace)
                }
            }
        })
        ```
      </Tab>

      <Tab title="React Native">
        ```javascript theme={null}
        adapty.addEventListener('onInstallationDetailsSuccess', details => {
            try {
                if (!details.payload) return;
                const payload = JSON.parse(details.payload);
                const userId = payload.ios_deferred_data || payload.android_deferred_data;
                if (userId) {
                    // Identify user with userId
                }
            } catch (error) {
                console.error('Error parsing installation details payload:', error);
            }
        });
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="AppsFlyer">
    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:

    1. Listen for the callback from AppsFlyer SDK.
    2. Extract `ffkey` from the conversion data.
    3. Identify/authenticate the user using the extracted key.

    For complete implementation details, refer to the [AppsFlyer documentation](https://dev.appsflyer.com/hc/docs/dl_getting_started).

    **Code examples**:

    <Tabs>
      <Tab title="Swift">
        ```swift theme={null}
        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)")
            }
        }
        ```
      </Tab>

      <Tab title="Kotlin">
        ```kotlin theme={null}
        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?) {}
        }
        ```
      </Tab>

      <Tab title="React Native">
        ```javascript theme={null}
        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.
          }
        });
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="Adjust">
    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:

    1. Enable the deferred deep link callback in the Adjust SDK.
    2. Parse the `user_id` query parameter from the returned URL.
    3. Identify/authenticate the user using `user_id` (e.g., with Adapty or your backend).

    For complete implementation details, refer to the [Adjust documentation](https://help.adjust.com/en/article/deep-links).

    **Code examples**:

    <Tabs>
      <Tab title="Swift">
        ```swift theme={null}

        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
         }
        ```
      </Tab>

      <Tab title="Kotlin">
        ```kotlin theme={null}
        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
        }
        }
        }

        ```
      </Tab>

      <Tab title="React Native">
        ```javascript theme={null}

        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);

        ```
      </Tab>
    </Tabs>
  </Tab>
</Tabs>
