# Adjust Migration

This guide is for teams moving from Adjust (Adjust Link / TrueLink) to WarpLink: mapping trackers and link 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 Adjust.

## Concept Mapping

| Adjust | WarpLink |
|--------|----------|
| Tracker (Adjust Link / TrueLink) | Link |
| Adjust dashboard | [WarpLink dashboard](https://dash.warplink.app) |
| `adj.st` subdomain or `*.go.link` branded domain | `yourapp.aplnk.to` (your app's subdomain) or a custom domain |
| Enterprise Domain | [Custom domain](https://warplink.app/docs/concepts/custom-domains) |
| `AdjustDelegate` deferred deep link callback | `onLink` callback |
| Tracker URL parameters (`deep_link`, `redirect`, `label`, ...) | Link fields |
| Datascape reporting | Built-in click analytics |

## 1. Export Your Links

**Tracker list via API.** Adjust's Campaign API includes an endpoint that lists trackers for a given app, with cursor-based pagination, so you can pull your tracker list and their parameters programmatically.

**Raw click and event data.** Adjust exports raw and historical data two ways: real-time server callbacks to your own servers or BI system, or hourly CSV uploads to a customer-owned cloud storage bucket (S3, GCS, or similar), configured under **Dashboard > App settings > All Settings > Raw Data Exports > CSV Upload**.

**Dashboard export.** Adjust's Datascape reporting UI lets you filter, visualize, and export reports directly from the dashboard.

## 2. Recreate Your Links

Recreate each Adjust tracker as a link in WarpLink via the dashboard or the REST API.

### Parameter Mapping

| Adjust Parameter | WarpLink Field |
|-------------------|-----------------|
| `deep_link` (direct in-app route, supersedes every other redirect parameter) | `ios_url` / `android_url` |
| `redirect` (override destination) | `destination_url` |
| `redirect_ios` / `redirect_android` (platform-specific redirect override) | `ios_fallback_url` / `android_fallback_url` |
| `fallback` (redirect for an unsupported OS) | `destination_url` |
| `label` (free-form passthrough attached to raw data exports) | `tags` |
| `adj_campaign` (short-link append parameter) | `utm_campaign` |

Use `tags` to organize links in the dashboard. On Adjust short links (`adj.st`/`go.link` URLs already created), the `adj_redirect` and `adj_fallback` query parameters are recognized names but cannot override that short link's destination or fallback, for security reasons. Only tracker-level `redirect` and `fallback` control where a link sends a user.

Carry `adj_adgroup` and `adj_creative` values as `destination_url` query parameters.

### 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",
    "ios_url": "myapp://product/123",
    "android_url": "myapp://product/123",
    "utm_campaign": "spring_sale"
  }'
```

## 3. Swap the SDK

**Remove Adjust:**
1. Remove the `Adjust` CocoaPods pod (`pod 'Adjust', '~> 4.38.4'`) or the SPM package from `https://github.com/adjust/ios_sdk` (product `AdjustSdk`)
2. Remove `ADJConfig(appToken:environment:)` and the `Adjust.initSdk(_:)` call
3. Remove the `AdjustDelegate` conformance and `adjustDeferredDeeplinkReceived(_:)`
4. Remove the deep link forwarding calls that exist solely to reach Adjust: `application(_:continue:restorationHandler:)`, `application(_:open:options:)`, `scene(_:willConnectTo:options:)`, `scene(_:continue:)`, `scene(_:openURLContexts:)`, or SwiftUI's `.onOpenURL`

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

**Remove Adjust:**
```kotlin
// Remove from build.gradle.kts
// implementation("com.adjust.sdk:adjust-android:5.8.0")
// implementation("com.android.installreferrer:installreferrer:<version>")
// implementation("com.google.android.gms:play-services-ads-identifier:<version>")
```
Also remove the `AdjustConfig(context, appToken, environment)` / `Adjust.initSdk(config)` calls and any `Adjust.processDeeplink(...)` or `Adjust.processAndResolveDeeplink(...)` calls in your Activity.

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

**Remove Adjust:**
```bash
npm uninstall react-native-adjust
```

**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 Adjust:**
```bash
flutter pub remove adjust_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 (Adjust):**
```swift
import AdjustSdk

let adjustConfig = ADJConfig(appToken: "{YourAppToken}", environment: ADJEnvironmentProduction)
Adjust.initSdk(adjustConfig)
```

**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 (Adjust):**
```kotlin
import com.adjust.sdk.Adjust
import com.adjust.sdk.AdjustConfig

val config = AdjustConfig(this, "{YourAppToken}", AdjustConfig.ENVIRONMENT_PRODUCTION)
Adjust.initSdk(config)
```

**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 (Adjust):**
```tsx
import { Adjust, AdjustConfig } from 'react-native-adjust';

const adjustConfig = new AdjustConfig('{YourAppToken}', AdjustConfig.EnvironmentProduction);
Adjust.initSdk(adjustConfig);
```

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

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

**Before (Adjust):**
```dart
import 'package:adjust_sdk/adjust.dart';
import 'package:adjust_sdk/adjust_config.dart';

final adjustConfig = AdjustConfig('{YourAppToken}', AdjustEnvironment.production);
Adjust.initSdk(adjustConfig);
```

**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:f2k5.adj.st
+ applinks:yourapp.aplnk.to
```

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

```diff
  <data
      android:scheme="https"
-     android:host="f2k5.adj.st" />
+     android:host="yourapp.aplnk.to" />
```

`adj.st` and branded `*.go.link` domains are Adjust-owned. The branded domain name cannot be changed once saved, and neither domain can be repointed to another provider, so migrating off either means standing up a new domain on WarpLink and updating every published link. An Enterprise Domain is different: it is your own subdomain, wired to Adjust with an A record in your own DNS. Adjust's own configuration for that domain cannot be changed without contacting Adjust support, but the DNS record itself is yours, so once your links exist on WarpLink you can replace that record (delete Adjust's A record and add WarpLink's CNAME) and keep the domain and its already-distributed links intact. See [custom domains](https://warplink.app/docs/concepts/custom-domains).

Keep your existing Adjust Associated Domains and App Links entries in place alongside the new WarpLink ones during the transition, the same approach Adjust itself recommends when adding a new branded domain. This keeps links already distributed (emails, ads, social bios, QR codes) resolving on-device while you cut over.

If Android App Links fail to verify after cutover, the most common causes are a signing-key fingerprint mismatch between `assetlinks.json` and the APK's actual signature (especially with Play App Signing re-signing), a missing `android:autoVerify="true"`, or `assetlinks.json` being served via a redirect instead of a direct response.

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 `Adjust.processAndResolveDeeplink(_:withCompletionHandler:)` (or legacy `Adjust.processDeeplink(_:)`) call and the `ADJDeeplink` construction around it. 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 `Adjust.processDeeplink(AdjustDeeplink, Context)` (or `Adjust.processAndResolveDeeplink(...)`) call from `onCreate` / `onNewIntent`. 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 `Adjust.processDeeplink(...)` calls from your `Linking` listeners. Cold and warm start flow into `onLink` automatically.

On iOS you are removing the Adjust call from a delegate method that already exists and already forwards to your linking library. Swap the Adjust 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 Adjust.processDeeplink(...)
  }
  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 `Adjust.processDeeplink(AdjustDeeplink(url))` (or `Adjust.processAndResolveDeeplink(...)`) calls from your link listeners. 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 Adjust 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.

Adjust's deferred delivery runs through the `AdjustDelegate` protocol's `adjustDeferredDeeplinkReceived(_ deeplink: URL?) -> Bool` method on iOS, registered via `adjustConfig.delegate` before `Adjust.initSdk(...)`, through `setDeferredDeeplinkCallback` on `AdjustConfig` in React Native, and through `deferredDeeplinkCallback` on `AdjustConfig` in Flutter. Delete those; WarpLink's `onLink` replaces all of them. On Android, remove whatever deferred handling code you added alongside the SDK itself in step 3.

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.

If you run Adjust and WarpLink side by side during the transition, avoid registering both SDKs for the same URL scheme or Associated Domains entry. Running two attribution SDKs against the same domain risks double-counting installs or conflicting deep link handling; stagger the cutover by app version rather than leaving both SDKs live indefinitely.

Beyond that, 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 Adjust. 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 Adjust 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.
- [Adjust alternative for small teams](https://warplink.app/blog/adjust-alternative-for-small-teams): cost comparison and positioning for teams evaluating the move.
