# Kochava Migration

This guide is for teams moving from Kochava (SmartLinks) to WarpLink: mapping SmartLink parameters, exporting existing links and data, and swapping the Kochava SDK for the WarpLink SDK on each platform, carrying over deep linking, install attribution, and analytics. 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 Kochava.

## Concept Mapping

| Kochava | WarpLink |
|---------|----------|
| SmartLink | Link |
| Kochava dashboard | [WarpLink dashboard](https://dash.warplink.app) |
| `your-brand-name.smart.link` subdomain | `yourapp.aplnk.to` (your app's subdomain) or a custom domain |
| Deep link destination / catch-all macro (`{insert_deeplink_here}`, `{deeplink}`) | `ios_url` / `android_url` |
| Custom Link Previews | `og_title` / `og_description` / `og_image_url` |
| Custom Parameter (key/value catch-all) | A query parameter appended to `destination_url`, echoed back as `custom_params` on resolve |

## 1. Export Your Links

**Links via API.** Kochava's Links API (`https://links.api.kochava.com/v1`, header auth `Authentication-Key: {MyUserAPIKey}`) exposes `GET /links` to list your SmartLinks with filtering and pagination, and `GET /links/{id}` to fetch one. Returned fields include `id`, `guid`, `name`, `click_url`, `impression_url`, `custom_params`, `routes_by_app`, `destination_url`, `reengagement_url`, and `native_link`. There is no documented bulk CSV export of link configurations, so plan to page through `GET /links` and reconstruct a spreadsheet yourself.

**Historical click and attribution data.** A separate Reporting API accepts `time_start`/`time_end` and returns CSV or JSON, with an option to have Kochava deposit reports directly into a customer-owned S3 bucket instead of polling.

Kochava's SmartLink creation guide does not publish a slug or custom-alias format for SmartLinks. Pull your actual SmartLink IDs from the Links API before assuming they will carry over as WarpLink custom slugs.

## 2. Recreate Your Links

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

### Parameter Mapping

| Kochava Field | WarpLink Field |
|-----------------|-----------------|
| Deep link destination (Custom / Deeplink, macro-based) | `ios_url` / `android_url` |
| Destination if App is Installed (per-platform) | `ios_url` / `android_url` |
| Destination if App is Not Installed / catch-all for unsupported platforms | `destination_url` |
| Custom Link Preview image / title / description | `og_title` / `og_description` / `og_image_url` |
| Custom Parameter (key/value catch-all) | A query parameter appended to `destination_url`, echoed back as `custom_params` on resolve |
| UTM parameters | `utm_campaign` / `utm_medium` / `utm_source` |

Kochava's deep link macro names are case-sensitive: the placeholder in the click URL (for example `{insert_deeplink_here}`) must match the destination string exactly. When you move that value into `ios_url` or `android_url`, drop the macro syntax and put the resolved app-scheme path directly in the field.

### 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",
    "og_title": "Check out this product",
    "utm_campaign": "spring_sale"
  }'
```

## 3. Swap the SDK

**Remove Kochava:**
1. Remove the `KochavaMeasurement` Swift Package (`https://github.com/Kochava/Apple-SwiftPackage-KochavaMeasurement-XCFramework`) along with `KochavaNetworking` and, if present, the optional `KochavaTracking` module
2. Remove the `Measurement.shared.start(appGUIDString:)` call (or its Objective-C equivalent, `KVAMeasurement.shared` / `startWithAppGUIDString:`)
3. Remove any `KVADeeplink` processing calls wired into your URL and Universal Link handlers

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

**Remove Kochava:**
```kotlin
// Remove from build.gradle.kts
// implementation("com.kochava.measurement:base:$kochava_measurement_version")
```
Also remove the `Measurement.getInstance().startWithAppGuid(context, appGuid)` call and any `Measurement.getInstance().processDeeplink(...)` calls in your Activity, plus the intent filter entry for `your-brand-name.smart.link`.

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

**Remove Kochava:**
```bash
npm uninstall react-native-kochava-measurement
```

**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 Kochava:**
```bash
flutter pub remove kochava_tracker
```

**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 (Kochava):**
```swift
import KochavaMeasurement

let measurement = Measurement.shared
measurement.start(appGUIDString: "_YOUR_APP_GUID_")
```

**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 (Kochava):**
```kotlin
import com.kochava.measurement.Measurement

Measurement.getInstance().startWithAppGuid(applicationContext, "YOUR_APP_GUID")
```

**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 (Kochava):**
```tsx
import { KochavaMeasurement } from 'react-native-kochava-measurement';

KochavaMeasurement.instance.registerAndroidAppGuid('YOUR_ANDROID_APP_GUID');
KochavaMeasurement.instance.registerAppleAppGuid('YOUR_APPLE_APP_GUID');
KochavaMeasurement.instance.start();
```

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

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

**Before (Kochava):**
```dart
import 'package:kochava_tracker/kochava_tracker.dart';

KochavaTracker.instance.registerAndroidAppGuid('YOUR_ANDROID_APP_GUID');
KochavaTracker.instance.registerIosAppGuid('YOUR_APPLE_APP_GUID');
KochavaTracker.instance.start();
```

**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:your-brand-name.smart.link
+ applinks:yourapp.aplnk.to
```

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

```diff
  <data
      android:scheme="https"
-     android:host="your-brand-name.smart.link" />
+     android:host="yourapp.aplnk.to" />
```

`smart.link` is Kochava-owned infrastructure. Every SmartLink domain, including a branded `your-brand-name.smart.link` subdomain, is provisioned by Kochava's Client Success team on Kochava's own infrastructure, and Kochava hosts the AASA and Digital Asset Links files for it directly. There is no documented option for a fully customer-owned root domain pointed at Kochava through your own DNS. That means migrating off Kochava is not a DNS repoint: plan for a new link domain on WarpLink from day one, and update every published link (ads, emails, social bios, QR codes, App Store metadata) to the new URL rather than expecting the old one to carry over. Whether Kochava continues serving your `smart.link` subdomain after you cancel is not documented; confirm directly with Kochava before assuming old links stay live during your transition window.

Moving to a [custom domain](https://warplink.app/docs/concepts/custom-domains) on WarpLink instead of your app's `yourapp.aplnk.to` subdomain? 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).

Keep your existing Kochava Associated Domains and App Links entries in place alongside the new WarpLink ones while you cut over, so links already distributed keep resolving on-device until every surface is updated to the new domain.

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.

## 6. Migrate Deep Link Handling

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

Kochava's own deep-linking guide does not publish exact `continue userActivity` / `open url` wiring for iOS, so check your existing integration for however your team called into `Deeplink.process(url:...)` and delete it. Forward the incoming activity to WarpLink instead (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 `Measurement.getInstance().processDeeplink(path, eventAutoSend, listener)` call from your Activity. Cold start is automatic once `onLink` is set at `configure()`. 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) // was Measurement.getInstance().processDeeplink(...)
}
```

Resolved links arrive in your `onLink` callback.

Delete the `KochavaMeasurement.instance.processDeeplink(path, eventAutoSend)` calls from your `Linking` listeners. Cold and warm start flow into `onLink` automatically.

On iOS you are removing the Kochava call from a delegate method that already exists and already forwards to your linking library. Swap the Kochava 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 KochavaMeasurement.instance.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 `KochavaTracker.instance.processDeeplink(path)` 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 Kochava 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.

Kochava treats deep links and deferred deep links the same way from an SDK perspective: install-state at click time is the only difference, and both flow through the same deep link processing call rather than a separate deferred callback. There is nothing further to remove beyond the calls deleted in step 6.

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 ?: 0.0}")
        }
        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 link guides for [iOS](https://warplink.app/docs/sdks/ios/deferred), [Android](https://warplink.app/docs/sdks/android/deferred), [React Native](https://warplink.app/docs/sdks/react-native/deferred), and [Flutter](https://warplink.app/docs/sdks/flutter/deferred)).

## 8. Attribution and Reporting

WarpLink attributes an install to the link that drove it 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 platform attribution guides for [iOS](https://warplink.app/docs/sdks/ios/attribution), [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) for match types, confidence bands, and privacy details.

If you run Kochava 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.

Kochava's own case-sensitivity warning for deep link macros (the placeholder name in the click URL must match the destination string exactly) is a common source of broken links when hand-porting an existing SmartLink into a new provider's parameter syntax. Double-check every ported link's destination once it is live on WarpLink, not just its configuration.

## 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
- [ ] Every ported deep link macro resolves to the correct destination, not just the literal placeholder string
- [ ] All Kochava 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.
- [Kochava alternative for small app teams](https://warplink.app/blog/kochava-alternative-for-small-teams): how WarpLink compares to Kochava.
