# Branch Migration

This guide is for teams currently using Branch for links, deep linking, install attribution, or analytics who are moving to WarpLink. It covers Branch-specific concept mapping, exporting your existing links and data, and swapping the Branch SDK for the WarpLink SDK on each platform. For the provider-agnostic playbook (inventory, domain planning, cutover, and decommissioning), see [Migration Overview](https://warplink.app/docs/migration).

## Concept Mapping

| Branch Concept | WarpLink Concept |
|-----------------|-------------------|
| Quick Link / Ad Link | Link |
| Alias | Slug |
| `app.link` / `bnc.lt` domain | `yourapp.aplnk.to` (your app's subdomain) or a custom domain |
| Custom domain (CNAME or NS delegation) | [Custom domain](https://warplink.app/docs/concepts/custom-domains) |
| `$`-prefixed link parameters | Link fields (see parameter mapping below) |
| LinkHub (Dashboard → Manager) | [WarpLink dashboard](https://dash.warplink.app) Links page |

## 1. Export Your Links

Branch's **LinkHub** (Dashboard → Manager) lets you search, filter, and bulk export both Quick Links and Ad Links, meaning dashboard-created and SDK/API-generated links are both reachable there. If you need to script an export instead, the Read endpoint returns one link's configuration at a time:

```http
GET /v1/url?url=<url>&branch_key=<branch key>
```

There is no single Branch endpoint that exports all historical links and all historical click analytics together. Link configuration and click/event data are separate export surfaces:

- **Dashboard Exports**: an ad hoc CSV of whatever report you are currently viewing.
- **Daily Exports API** (`POST https://api2.branch.io/v3/export`): gzipped CSV, up to 200,000 rows per file, capped at 7 days per request (1 day recommended), and only accessible via the API for 6 months after the data posts.
- **Custom Exports API**: covers install, commerce, content, user-lifecycle, custom, and fraud events as CSV or JSON, up to 2,000,000 records per request with a 180-day lookback and a 60-day query window per request. Rate limited to 10 requests/minute and 25/hour, so a full-account export has to be paced across multiple calls.

Branch's device-matching attribution data is deleted after 30 days of user inactivity (90 days for some attribution products). Pull your historical exports before that window closes, not after.

## 2. Recreate Your Links

Recreate your Branch links in WarpLink via the dashboard or the REST API.

### Parameter Mapping

| Branch Parameter | WarpLink Field |
|-------------------|----------------|
| Alias | `slug` |
| `$fallback_url` | `destination_url` |
| `$ios_url` | `ios_fallback_url` |
| `$android_url` | `android_fallback_url` |
| `$deeplink_path` / `$ios_deeplink_path` | `ios_url` |
| `$android_deeplink_path` | `android_url` |
| `$og_title` | `og_title` |
| `$og_description` | `og_description` |
| `$og_image_url` | `og_image_url` |
| `$exp_date` | `expires_at` |
| `$match_duration` | `match_window_hours` |
| Repeated `~tags` query parameters | `tags` |
| `password` (Branch reserved query param) | `password` |
| Other link data (custom key/value pairs) | Encoded as query parameters on `destination_url`, echoed back as `custom_params` on resolve |

Carry any other Branch link data over 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",
    "ios_fallback_url": "https://apps.apple.com/app/id123456789",
    "android_url": "myapp://product/123",
    "android_fallback_url": "https://play.google.com/store/apps/details?id=com.example.myapp",
    "og_title": "Check out this product",
    "expires_at": "2027-12-31T00:00:00Z"
  }'
```

## 3. Swap the SDK

**Remove Branch:**
1. Remove the `BranchSDK` package (CocoaPods `pod 'BranchSDK'`, SPM `https://github.com/BranchMetrics/ios-branch-sdk-spm`, or Carthage `github "BranchMetrics/ios-branch-deep-linking"`)
2. Remove the `Branch.getInstance()` / `initSession(launchOptions:)` call
3. Remove the `branch_key` and `branch_universal_link_domains` keys from `Info.plist`
4. Remove the `applinks:subdomain.app.link` Associated Domains entries (including the `-alternate` and `.test` variants)

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

**Remove Branch:**
```kotlin
// Remove from build.gradle.kts
// implementation("io.branch.sdk.android:library:<version>")
```
Also remove the `io.branch.sdk.BranchKey`, `io.branch.sdk.BranchKey.test`, and `io.branch.sdk.TestMode` manifest meta-data entries, and the custom URI scheme and HTTPS App Links intent filters pointed at your `app.link` domains.

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

**Remove Branch:**
```bash
npm uninstall react-native-branch
cd ios && pod install
```

**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 Branch:**
```bash
flutter pub remove flutter_branch_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

**Use an SDK key here, not an API key.** Mobile apps authenticate with an **SDK key**, created in the dashboard under **Keys** > **SDK keys**. Only an SDK key records installs.

**Before (Branch):**
```swift
Branch.getInstance().initSession(launchOptions: launchOptions)
```

**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 (Branch):**
```kotlin
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Branch.getAutoInstance(this)
    }
}
```

**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 (Branch):**
```tsx
import branch from 'react-native-branch';

branch.subscribe(({ error, params }) => {
    if (error) return;
    // handle params
});
```

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

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

**Before (Branch):**
```dart
import 'package:flutter_branch_sdk/flutter_branch_sdk.dart';

await FlutterBranchSdk.init();
FlutterBranchSdk.listSession().listen((data) {
  // handle data
});
```

**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:yourapp.app.link
+ applinks:yourapp.aplnk.to
```

Remove every `applinks:` entry for your `app.link` (and legacy `bnc.lt`) subdomain, including the `-alternate` and `.test` variants Branch adds automatically.

In `AndroidManifest.xml`, update the intent filter host and remove Branch's custom URI scheme filter:

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

Remove the `android:autoVerify="true"` intent filters for both your live and test `app.link` domains.

Moving your links to a [custom domain](https://warplink.app/docs/concepts/custom-domains) instead? Use that host here, and declare it to the SDK as well: `linkDomains` at `configure()`, or the `WarpLinkDomains` plist key / `app.warplink.DOMAINS` manifest entry. That is what makes a custom-domain link resolve on the first launch. See [Use it in your app](https://warplink.app/docs/concepts/custom-domains#use-it-in-your-app).

Branch's default `app.link` and `bnc.lt` domains cannot move with you; only your own custom domain can be repointed. If your custom domain was set up as a Branch CNAME subdomain, your registrar was never handed off, so the CNAME can be repointed to WarpLink once your links exist there. If it was set up as a root-domain NS delegation, Branch's nameservers are authoritative for that domain, so regaining control means moving the NS records back to your registrar (or another DNS provider) before pointing it at WarpLink, a normal DNS change but one Branch does not document as an offboarding step. Branch hosts the AASA file and TLS certificate for both setups; once you repoint to WarpLink, WarpLink issues and renews the certificate for your verified custom domain automatically.

## 6. Migrate Deep Link Handling

Delete the Branch calls inside `application(_:open:options:)` and `application(_:continue:restorationHandler:)` (and the SceneDelegate equivalents, `scene(_:continue:)` and `scene(_:openURLContexts:)`, if your app uses scenes). Forward the same URL or activity to WarpLink instead:

```swift
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    WarpLink.open(url) // was Branch's handler
    return true
}

func application(
    _ application: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
    WarpLink.continue(userActivity) // was Branch's handler
    return true
}
```

Resolved links arrive in your `onLink` callback. Or set your delegate class to the drop-in `WarpLinkAppDelegate` / `WarpLinkSceneDelegate` and skip writing these methods yourself.

Delete the `Branch.sessionBuilder(this).withCallback { ... }.withData(intent.data).init()` call from your Activity's `onStart()`. Cold start is automatic once `onLink` is set at `configure()`. For warm start, forward new intents and keep `android:launchMode="singleTask"` on the Activity:

```kotlin
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    WarpLink.onNewIntent(intent) // was Branch's onStart() session
}
```

Resolved links arrive in your `onLink` callback.

Delete the `branch.subscribe(...)` callback. `WarpLink.configure()`'s `onLink` callback covers cold start, warm start, and deferred installs in one callback, the same way `subscribe` did.

On iOS you are removing the Branch line from a delegate method that already exists and already forwards to your linking library. Swap it 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 Branch's handler
  }
  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`.

Delete the `FlutterBranchSdk.listSession().listen(...)` subscription. `WarpLink.configure()`'s `onLink` callback covers cold start, warm start, and deferred installs in one callback, the same way `listSession` did.

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 Branch 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

Branch's deferred deep linking on iOS 15+ with Private Relay enabled relies on NativeLink (the `$ios_nativelink` link parameter), delivered through the same session-init callback as a direct tap. WarpLink runs its deferred check automatically from `configure()` and delivers the match to the same `onLink` callback, flagged with `isDeferred`:

```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)
})
```

Branch delivers a deferred match through the same `sessionBuilder().init()` callback used for a direct link open. WarpLink runs its deferred check automatically from `configure()` and delivers the match to the same `onLink` callback, flagged with `isDeferred`:

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

Branch's `subscribe` callback also surfaces deferred params on cold start: the result is cached natively and delivered if `subscribe` is called within a TTL (default 5000ms, configurable via `branch.initSessionTtl`). WarpLink runs its deferred check automatically from `configure()` and delivers the match to the same `onLink` callback, flagged with `isDeferred`, with no TTL to configure:

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

Branch delivers deferred params through the same `listSession()` stream. WarpLink runs its deferred check automatically from `configure()` and delivers the match to the same `onLink` callback, flagged with `isDeferred`:

```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 with a multi-tier match cascade (referrer, device ID, and probabilistic matching), returning a match type and confidence score you can use to decide how to route the user. See [Install Attribution](https://warplink.app/docs/concepts/attribution) for the full match cascade and confidence scoring, and 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).

Branch does not publish guidance for running its SDK alongside another provider during a migration off Branch (its published migration guides only cover switching onto Branch from another vendor). A few gotchas to test for during a dual-SDK window:

- **iOS AASA caching.** iOS caches the `apple-app-site-association` file, and a fresh fetch is typically triggered by an app reinstall, so a device that already resolved a Branch link may not immediately pick up your new WarpLink Associated Domains entry.
- **Android App Links verification.** Every manifest-referenced domain must host its own `.well-known/assetlinks.json`, and the `sha256_cert_fingerprints` value must exactly match your release signing key (uppercase), including Play App Signing's value if you use it. An http/https or `www` mismatch also breaks verification. Force a re-check with `adb shell pm verify-app-links --re-verify`.

## 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 Branch code removed: no remaining imports or dependencies
- [ ] Clean build: no compilation errors

## Next Steps

- [Migration Overview](https://warplink.app/docs/migration): the provider-agnostic playbook for 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.
- [Branch.io alternative for indie developers](https://warplink.app/blog/branch-io-alternative) and [Branch pricing compared](https://warplink.app/blog/branch-pricing): how WarpLink compares to Branch.
