# AppsFlyer Migration

This guide is for teams moving from AppsFlyer OneLink to WarpLink: mapping OneLink links and parameters, swapping the SDK, and carrying deep linking, install attribution, and analytics over to WarpLink. It assumes a WarpLink account and app are already set up.

For the provider-agnostic playbook (planning, domains, cutover, decommissioning), see the [general migration guide](https://warplink.app/docs/migration). This page covers only what is specific to leaving AppsFlyer OneLink.

## Concept Mapping

| AppsFlyer OneLink | WarpLink |
|--------------------|----------|
| OneLink template | Link |
| AppsFlyer dashboard | [WarpLink dashboard](https://dash.warplink.app) |
| `onelink.me` domain | `yourapp.aplnk.to` (your app's subdomain) or a custom domain |
| Branded domain (CNAME to a OneLink subdomain) | [Custom domain](https://warplink.app/docs/concepts/custom-domains) |
| Unified Deep Linking (`onDeepLink` callback) | `onLink` callback (on React Native and Flutter, not the separate `onDeepLink` stream, which is only for manual wiring) |
| `deep_link_value` / `deep_link_sub1`-`deep_link_sub10` | Query parameters on `destination_url`, echoed back as `custom_params` |

WarpLink always attempts the app open (Universal Link or App Link) when the app is installed, and falls back to the link's fallback URL or the app store only when it is not. See [Fallback Cascade](https://warplink.app/docs/concepts/deep-linking#fallback-cascade-2-tiers).

## 1. Export Your Links

**Dashboard CSV export** downloads a CSV of OneLink URLs per template. It only includes links created from the dashboard or a template: links created via the SDK, the OneLink REST API, or bulk upload do not appear in that export.

To get those links out, use the **OneLink REST API** (current version v2.0) to read them back programmatically. Access is not self-serve: it requires a customer success manager or an email to hello@appsflyer.com to enable it.

For historical click and conversion data, AppsFlyer's **Raw Data Pull API** caps downloads at 1,000,000 rows per request; larger histories need to be split into narrower time windows. Pull the data you need before decommissioning the account, since there is no single export that covers both link configuration and historical click data together.

## 2. Recreate Your Links

Recreate each OneLink link in WarpLink via the dashboard or the REST API.

### Parameter Mapping

| AppsFlyer Parameter | WarpLink Field |
|----------------------|-----------------|
| `deep_link_value` (in-app routing value) | `ios_url` / `android_url` |
| `af_dp` (legacy URI-scheme deep link) | `ios_url` / `android_url` |
| `af_web_dp` (web/social preview target) | `destination_url` |
| `af_ios_url` / `af_android_url` (fallback URLs) | `ios_fallback_url` / `android_fallback_url` |
| `deep_link_sub1` … `deep_link_sub10` (auxiliary values) | Query parameters on `destination_url`, echoed back as `custom_params` on resolve |
| `pid` (media source) / `c` (campaign) | `utm_source` / `utm_campaign` |
| Custom title, description, image (set in the OneLink template UI) | `og_title` / `og_description` / `og_image_url` |

`pid` and `c` map to the `utm_source` and `utm_campaign` fields for campaign tracking.

### Create via API

```bash
curl -X POST https://api.warplink.app/v1/links \
  -H "Authorization: Bearer wl_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_url": "https://yourapp.com/product/123?deep_link_sub1=campaign_spring",
    "ios_url": "myapp://product/123",
    "android_url": "myapp://product/123",
    "utm_source": "facebook",
    "utm_campaign": "spring_sale"
  }'
```

Custom slugs are 2 to 100 characters, lowercase alphanumeric with hyphens. AppsFlyer's OneLink URL ID defaults to 8 characters and allows up to 50; if you are scripting the migration, decide on a slug mapping up front for any URL ID that does not fit WarpLink's format.

## 3. Swap the SDK

**Remove AppsFlyer:**
1. Remove the `AppsFlyerFramework` package (CocoaPods, or SPM: `AppsFlyerSDK/AppsFlyerFramework` on SDK versions before 6.12.0, `AppsFlyerFramework-Static`/`-Dynamic`/`-Strict` on 6.12.0 and later)
2. Remove `AppsFlyerLib.shared().appsFlyerDevKey`, `.appleAppID`, and the `.start()` call
3. Remove the `NSAdvertisingAttributionReportEndpoint` and `AdAttributionKit` keys from Info.plist
4. Remove the `AppsFlyerDeepLinkDelegate` conformance and `didResolveDeepLink(_:)` (or the legacy `onAppOpenAttribution`)

**Add WarpLink:**
```swift
// Package.swift
dependencies: [
    .package(url: "https://github.com/WarpLinkApp/warplink-ios-sdk", from: "1.2.0")
]
```

**Remove AppsFlyer:**
```kotlin
// Remove from build.gradle.kts
// implementation("com.appsflyer:af-android-sdk:<version>")
```
Also remove the `MultipleInstallBroadcastReceiver` manifest entry, the `AppsFlyerLib.getInstance().init(...)` / `.start(context)` calls, and any `DeepLinkListener` implementation.

**Add WarpLink:**
```kotlin
dependencies {
    implementation("app.warplink:sdk:1.2.0")
}
```

**Remove AppsFlyer:**
```bash
npm uninstall react-native-appsflyer
```

**Add WarpLink:**
```bash
npm install @warplink/react-native
```

The iOS SDK is distributed through Swift Package Manager and needs dynamic linkage, so add this to `ios/Podfile` inside your app target before installing pods:

```ruby
# ios/Podfile, inside your app target
use_frameworks! :linkage => :dynamic
```

```bash
cd ios && pod install
```

**Remove AppsFlyer:**
```bash
flutter pub remove appsflyer_sdk
```

**Add WarpLink:**
```bash
flutter pub add warplink_flutter
```

The iOS SDK is distributed through Swift Package Manager, so there is no Podfile change. If your Flutter version does not use Swift Package Manager by default, run `flutter config --enable-swift-package-manager` once.

## 4. Update SDK Initialization

Mobile apps authenticate with an **SDK key**, created in the dashboard under **Keys** > **SDK keys**. An API key looks identical but cannot record installs, so pasting one here leaves deep links working while attribution silently fails.

**Before (AppsFlyer):**
```swift
AppsFlyerLib.shared().appsFlyerDevKey = "<DEV_KEY>"
AppsFlyerLib.shared().appleAppID = "<APPLE_APP_ID>"
AppsFlyerLib.shared().start()
```

**After (WarpLink):**
```swift
import WarpLink

WarpLink.configure(
    apiKey: "wl_live_yoursdkkeyhere000000000000000000",
    options: WarpLinkOptions(onLink: { result in
        if case .success(let link) = result, let link {
            navigate(to: link) // taps AND deferred installs (check link.isDeferred)
        }
    })
)
```

**Before (AppsFlyer):**
```kotlin
AppsFlyerLib.getInstance().init(devKey, conversionListener, context)
AppsFlyerLib.getInstance().start(context)
```

**After (WarpLink):**
```kotlin
import app.warplink.WarpLink
import app.warplink.WarpLinkOptions

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        WarpLink.configure(
            context = this,
            apiKey = "wl_live_yoursdkkeyhere000000000000000000",
            options = WarpLinkOptions(onLink = { result ->
                result.onSuccess { link -> navigateTo(link) }
            })
        )
    }
}
```

**Before (AppsFlyer):**
```tsx
import appsFlyer from 'react-native-appsflyer';

appsFlyer.initSdk();
appsFlyer.startSdk();
```

Method names shown are `react-native-appsflyer`'s current documented API. A newer SDK version may rename these; check your installed version before porting the surrounding config.

**After (WarpLink):**
```tsx
import { WarpLink } from '@warplink/react-native';

WarpLink.configure({
    apiKey: 'wl_live_yoursdkkeyhere000000000000000000',
    onLink: ({ deepLink }) => deepLink && navigateTo(deepLink.destination),
});
```

**Before (AppsFlyer):**
```dart
import 'package:appsflyer_sdk/appsflyer_sdk.dart';

final appsflyerSdk = AppsFlyerSdk.instance;

await appsflyerSdk.registerDeepLinkListener(onDeepLinking: (result) {
  // handle result.deepLink
});
await appsflyerSdk.init(devKey: '{YourDevKey}', appId: '{YourAppleAppId}');
await appsflyerSdk.registerSessionReadyListener(() async {
  await appsflyerSdk.start();
});
```

Method names shown are `appsflyer_sdk`'s current documented API. A newer SDK version may rename these; check your installed version before porting the surrounding config.

**After (WarpLink):**
```dart
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:warplink_flutter/warplink_flutter.dart';

class _AppState extends State<App> {
  @override
  void initState() {
    super.initState();
    unawaited(
      WarpLink.configure(
        apiKey: 'wl_live_yoursdkkeyhere000000000000000000',
        onLink: (event) {
          if (event is! LinkEvent) return;
          final deepLink = event.deepLink;
          navigateTo(deepLink.deepLinkUrl ?? deepLink.destination);
        },
      ),
    );
  }
}
```

Call `configure()` once from your root widget's `initState`, without awaiting it. `deepLinkUrl` is the app route; `destination` is the web URL and serves only as the fallback. A link that launches the app can arrive before the first frame, so make `navigateTo` wait for the navigator, as in [With a Navigator Key](https://warplink.app/docs/sdks/flutter/deep-links#with-a-navigator-key).

## 5. Update Domain Configuration

In **Signing & Capabilities > Associated Domains**:

```diff
- applinks:yoursubdomain.onelink.me
+ applinks:yourapp.aplnk.to
```

In `AndroidManifest.xml`, update the intent filter host:

```diff
  <data
      android:scheme="https"
-     android:host="yoursubdomain.onelink.me" />
+     android:host="yourapp.aplnk.to" />
```

`onelink.me` is AppsFlyer-owned infrastructure: it cannot be repointed, so links on it stop resolving once you leave AppsFlyer. A branded custom domain is different, since its CNAME sits on your own DNS. Once your links exist on WarpLink with the same paths, repoint that domain's DNS to WarpLink and existing shares, QR codes, and bookmarks keep working. See [custom domains](https://warplink.app/docs/concepts/custom-domains).

Moving to a custom domain? Declare it to the SDK too: `linkDomains` at `configure()`, or the `WarpLinkDomains` plist key / `app.warplink.DOMAINS` manifest entry. See [Use it in your app](https://warplink.app/docs/concepts/custom-domains#use-it-in-your-app).

## 6. Migrate Deep Link Handling

The `onLink` callback set in step 4 already receives resolved links for cold start, warm start, and deferred installs.

Delete the `AppsFlyerDeepLinkDelegate` conformance and `didResolveDeepLink(_:)` (or the legacy `onAppOpenAttribution`). Forward the incoming activity to WarpLink (or set your scene delegate class to `WarpLinkSceneDelegate`):

```swift
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    WarpLink.continue(userActivity)
}
```

Resolved links arrive in your `onLink` callback.

Delete the `DeepLinkListener` implementation and its `onDeepLinking(deepLinkResult)` handling. Cold start is automatic. For warm start, forward new intents and set `android:launchMode="singleTask"` on the Activity:

```kotlin
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    WarpLink.onNewIntent(intent)
}
```

Resolved links arrive in your `onLink` callback.

Delete the `onDeepLink()` and `onAppOpenAttribution()` listeners. Cold and warm start flow into `onLink` automatically.

On iOS you are removing the AppsFlyer call from a delegate method that already exists and already forwards to your linking library. Swap the AppsFlyer line for the WarpLink call and leave the rest of the method alone. Do not let `handleIncomingURL` decide the return value: it returns nothing, and replacing the forwarded result breaks React Navigation, Expo Linking, and OAuth callbacks.

```swift
// ios/YourApp/AppDelegate.swift
import React
import warplink_react_native

func application(
  _ application: UIApplication,
  continue userActivity: NSUserActivity,
  restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
  if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
     let url = userActivity.webpageURL {
    WarpLinkModule.handleIncomingURL(url)   // was AppsFlyerLib...
  }
  return RCTLinkingManager.application(
    application, continue: userActivity, restorationHandler: restorationHandler)
}
```

Android needs no host code, given `android:launchMode="singleTask"`. See the [React Native SDK guide](https://warplink.app/docs/sdks/react-native#ios-appdelegate-hook) for the Objective-C form and Expo prebuild, and the [iOS SceneDelegate hook](https://warplink.app/docs/sdks/react-native#ios-scenedelegate-hook) if your app has a `SceneDelegate` (`Info.plist` declares `UIApplicationSceneManifest`): iOS stops calling the AppDelegate method above once a scene exists.

Delete the `registerDeepLinkListener(onDeepLinking: ...)` registration. Cold and warm start flow into `onLink` automatically.

Switch off Flutter's own link handling so it does not also route the link as a page. In `ios/Runner/Info.plist`:

```xml
<key>FlutterDeepLinkingEnabled</key>
<false/>
```

In `android/app/src/main/AndroidManifest.xml`, inside your main `<activity>`:

```xml
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
```

There is no delegate or Activity code to port. The plugin registers with Flutter's iOS and Android lifecycles, so any forwarding you added to `AppDelegate` or `MainActivity` for AppsFlyer can go. Set `minSdk = 26` in `android/app/build.gradle.kts`, and see the [Flutter SDK guide](https://warplink.app/docs/sdks/flutter) for the Associated Domains capability and the intent filter.

## 7. Migrate Deferred Deep Links

WarpLink runs the deferred deep link check automatically from `configure()`. The match arrives in the same `onLink` callback, flagged with `isDeferred`. There is no separate deferred call to port.

AppsFlyer's deferred delivery ran through the legacy `onAppOpenAttribution` callback on first open, or through Unified Deep Linking's `onDeepLink`, distinguishing a deferred match with `isDeferred()`. Delete those listeners; WarpLink's `onLink` replaces both.

Handle the deferred case inside your `onLink` callback:

```swift
options: WarpLinkOptions(onLink: { result in
    guard case .success(let deepLink) = result, let deepLink else { return }
    if deepLink.isDeferred {
        print("Match confidence: \(deepLink.matchConfidence ?? 0)")
    }
    navigate(to: deepLink)
})
```

Handle the deferred case inside your `onLink` callback:

```kotlin
options = WarpLinkOptions(onLink = { result ->
    result.onSuccess { deepLink ->
        if (deepLink.isDeferred) {
            Log.d("MyApp", "Match confidence: ${deepLink.matchConfidence}")
        }
        navigateTo(deepLink)
    }
})
```

Handle the deferred case inside your `onLink` callback:

```tsx
WarpLink.configure({
    apiKey: 'wl_live_yoursdkkeyhere000000000000000000',
    onLink: ({ deepLink }) => {
        if (!deepLink) return;
        if (deepLink.isDeferred) console.log('Match confidence:', deepLink.matchConfidence);
        navigateTo(deepLink.destination);
    },
});
```

Handle the deferred case inside your `onLink` callback:

```dart
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:warplink_flutter/warplink_flutter.dart';

class _AppState extends State<App> {
  @override
  void initState() {
    super.initState();
    unawaited(
      WarpLink.configure(
        apiKey: 'wl_live_yoursdkkeyhere000000000000000000',
        onLink: (event) {
          if (event is! LinkEvent) return;
          final deepLink = event.deepLink;
          if (deepLink.isDeferred) debugPrint('Match confidence: ${deepLink.matchConfidence}');
          navigateTo(deepLink.deepLinkUrl ?? deepLink.destination);
        },
      ),
    );
  }
}
```

To run the check manually instead, set the deferred opt-out flag and call `checkDeferredDeepLink()` yourself (see the [deferred deep links](https://warplink.app/docs/sdks/ios/deferred) guides).

## 8. Attribution and Reporting

WarpLink attributes installs to the link that drove them through a match cascade: an exact referrer signal (Play Install Referrer) on Android, an enriched probabilistic match on both platforms, and a device ID (IDFV) check on iOS that recognizes an install asking again, with a confidence score on every result. See the [iOS attribution guide](https://warplink.app/docs/sdks/ios/attribution) (or the [Android](https://warplink.app/docs/sdks/android/attribution), [React Native](https://warplink.app/docs/sdks/react-native/attribution), and [Flutter](https://warplink.app/docs/sdks/flutter/attribution) equivalents) for match types, confidence bands, and privacy details.

Running both SDKs for a period is a normal part of a migration: it lets you compare install counts and deep link behavior before retiring the old provider. See [Run the Cutover](https://warplink.app/docs/migration#6-run-the-cutover) in the general guide for the rollout sequencing.

## Testing Checklist

After migration, verify on each platform:

- [ ] SDK initializes: enable debug logging and check console output
- [ ] Deep links open the app: tap a WarpLink URL on a physical device
- [ ] Deep link data resolves: destination and custom parameters are correct
- [ ] Deferred deep links work: click link, install, verify match. An uninstall is enough to retest on either platform, because the completion marker goes with the app. That second install is reported as a reinstall and still counts as an install
- [ ] AASA/assetlinks verified: `curl https://yourapp.aplnk.to/.well-known/apple-app-site-association`
- [ ] Error handling: test invalid URLs, expired links, no connectivity
- [ ] All AppsFlyer code removed: no remaining imports or dependencies
- [ ] Clean build: no compilation errors

## Next Steps

- [General migration guide](https://warplink.app/docs/migration): the provider-agnostic playbook for planning, domains, cutover, and decommissioning.
- [Quickstart](https://warplink.app/docs/quickstart): create your first link and get an SDK key.
- [iOS SDK](https://warplink.app/docs/sdks/ios), [Android SDK](https://warplink.app/docs/sdks/android), [React Native SDK](https://warplink.app/docs/sdks/react-native), and [Flutter SDK](https://warplink.app/docs/sdks/flutter): full setup and API reference.
- [AppsFlyer alternative for small teams](https://warplink.app/blog/appsflyer-alternative-for-small-teams): cost comparison and positioning for teams evaluating the move.
