Branch Migration
Migrate from Branch to WarpLink: link and parameter mapping, exporting data, SDK swap, and deep linking, install attribution, and analytics.
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.
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 |
$-prefixed link parameters | Link fields (see parameter mapping below) |
| LinkHub (Dashboard → Manager) | WarpLink dashboard 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:
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
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:
- Remove the
BranchSDKpackage (CocoaPodspod 'BranchSDK', SPMhttps://github.com/BranchMetrics/ios-branch-sdk-spm, or Carthagegithub "BranchMetrics/ios-branch-deep-linking") - Remove the
Branch.getInstance()/initSession(launchOptions:)call - Remove the
branch_keyandbranch_universal_link_domainskeys fromInfo.plist - Remove the
applinks:subdomain.app.linkAssociated Domains entries (including the-alternateand.testvariants)
Add WarpLink:
// Package.swift
dependencies: [
.package(url: "https://github.com/WarpLinkApp/warplink-ios-sdk", from: "1.1.1")
]Remove Branch:
// 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:
dependencies {
implementation("app.warplink:sdk:1.1.1")
}Remove Branch:
npm uninstall react-native-branch
cd ios && pod installAdd WarpLink:
npm install @warplink/react-nativeThe 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:
# ios/Podfile, inside your app target
use_frameworks! :linkage => :dynamiccd ios && pod installRemove Branch:
flutter pub remove flutter_branch_sdkAdd WarpLink:
flutter pub add warplink_flutterThe 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):
Branch.getInstance().initSession(launchOptions: launchOptions)After (WarpLink):
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):
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
Branch.getAutoInstance(this)
}
}After (WarpLink):
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):
import branch from 'react-native-branch';
branch.subscribe(({ error, params }) => {
if (error) return;
// handle params
});After (WarpLink):
import { WarpLink } from '@warplink/react-native';
WarpLink.configure({
apiKey: 'wl_live_yoursdkkeyhere000000000000000000',
onLink: ({ deepLink }) => deepLink && navigateTo(deepLink.destination),
});Before (Branch):
import 'package:flutter_branch_sdk/flutter_branch_sdk.dart';
await FlutterBranchSdk.init();
FlutterBranchSdk.listSession().listen((data) {
// handle data
});After (WarpLink):
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.
5. Update Domain Configuration
In Signing & Capabilities > Associated Domains:
- applinks:yourapp.app.link
+ applinks:yourapp.aplnk.toRemove 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:
<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 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.
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:
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:
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.
// 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 for the Objective-C form and Expo prebuild, and the 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:
<key>FlutterDeepLinkingEnabled</key>
<false/>In android/app/src/main/AndroidManifest.xml, inside your main <activity>:
<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 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:
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:
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:
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:
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, Android, React Native, and Flutter).
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 for the full match cascade and confidence scoring, and the platform attribution guides for iOS, Android, React Native, and Flutter.
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-associationfile, 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 thesha256_cert_fingerprintsvalue must exactly match your release signing key (uppercase), including Play App Signing's value if you use it. An http/https orwwwmismatch also breaks verification. Force a re-check withadb 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: the provider-agnostic playbook for domains, cutover, and decommissioning.
- Quickstart: create your first link and get an SDK key.
- iOS SDK, Android SDK, React Native SDK, and Flutter SDK: full setup and API reference.
- Branch.io alternative for indie developers and Branch pricing compared: how WarpLink compares to Branch.
Your App's Own Link Address
Set up your app's own {handle}.aplnk.to link address, test it on a phone, and make it the default for new links. Existing links keep their addresses.
AppsFlyer Migration
Migrate from AppsFlyer OneLink to WarpLink: link and parameter mapping, SDK swap, and deep linking, install attribution, and analytics.