All field notes

How to move from Branch to Ferry.

Nothing about Branch is down. The links still resolve, the SDK still fires. What changed is the invoice. If you searched "branch io pricing" right after a renewal call, this guide is for the migration you're actually planning, not a shutdown you're reacting to.

OLD ROUTE acme.app.link/summer Branch
NEW ROUTE https://go.acme.com/summer Ferry link, payload preserved
MOVED
MAU
BILL

There's no self-serve price for a reason.

Branch's own pricing page doesn't show a dollar amount anywhere on it. There are three tiers, Basics, Essentials, and Enterprise, and only Basics is something you can actually start on your own, as a free trial capped at three ad-network partners and basic dashboards. Want unlimited partners, unlimited data webhooks, or anything on the Enterprise tier, and the button says "request a demo," not "upgrade."

Neither tier publishes what usage number actually moves the quote. If you searched "branch io pricing" and landed on a page with no number on it, that's not you missing something, that's the page working as designed. It's worth fixing with a pricing model, not just a migration: a provider whose cost tracks the thing you actually pay to route, links and the events attached to them.

Because Branch isn't shutting down, you get to run this migration at your own pace. There's no forced deadline, no exported-before-it's-deleted metadata file. That's an advantage. Use it to move link by link instead of all at once.

01

Start with what's actually driving the bill.

Before touching the SDK, find every place Branch is embedded: the initialization call, the deep link listener, any Branch Universal Object creation, and every custom event you send for attribution. Separately, list the links that still carry real traffic, campaigns, or referral value.

Build one working sheet with:

  • the Branch link and its destination or deep link path;
  • the custom metadata or content keys attached to it;
  • iOS, Android, and desktop fallback behavior;
  • campaign source, medium, and name;
  • where the link is published;
  • an owner and migration status.

This inventory does two jobs. It's your cutover checklist, and it's the evidence for whether the MAU number you were quoted on actually reflects link-driven traffic or just app usage that has nothing to do with linking.

Migrate the links with a job. Stop paying for the sessions that never used one.

02

Map the old model to Ferry.

The concepts translate closely, but Ferry doesn't ask you to construct a client-side object to describe a link. A project represents one app, a hostname belongs to one project, and every link carries structured data your app reads directly.

Branch Ferry Migration note
Branch key (key_live_ / key_test_) Project pk_ / sk_ API keys pk_ ships in the app for resolve and match. sk_ stays server-side for creating links.
Custom link domain or app.link Shared or custom hostname Keep a domain you own. Claim a feryl.io hostname if you were on a shared app.link subdomain.
Branch Universal Object + custom metadata payload.data The same key/value contract, without a client SDK object required to build it.
Deep link routing rules ($deeplink_path, controlParams) App-side routing from payload.data Your app already owns navigation. Ferry hands back data, not a route it dictates.
Branch.initSession listener Resolve and match Direct opens resolve a link. First opens after install call match.
MAU-based pricing tier Usage tied to links and events No charge for app opens that never touched a Ferry link.

Whatever you put in a Branch Universal Object's contentMetadata or custom params becomes payload.data in Ferry: the same { "screen": "offer", "campaign": "summer" } shape, just without an object you have to build on the client before creating a link.

04

The part you're dreading doesn't happen: your routing code doesn't change.

If your app is built the way most Branch integrations are, you already have a function that takes a data object and decides where to navigate. That function reads params["$deeplink_path"] or your own custom keys off whatever Branch handed back. It doesn't know or care that Branch produced that object. Swapping the producer is the whole migration.

A

App already installed

Configure Universal Links on iOS and App Links on Android. Ferry serves the AASA and assetlinks.json files for the project's hostname. Where you called Branch.getInstance().handleDeepLink(), call Ferry's resolve endpoint instead, then route from payload.data exactly like before.

B

App installed after click

Where you read Branch's first-session params in initSession, call POST /v1/match on first launch instead. Android can pass the Play Install Referrer. iOS can pass an App Clip or pasteboard click ID when available, with a coarse fingerprint as a probabilistic fallback.

Ferry returns a match method and confidence alongside the payload. Deterministic signals are preferred. If a fingerprint could refer to more than one click, Ferry returns no match instead of guessing.

FIRST OPEN RESPONSE 200 OK
{
  "matched": true,
  "method": "install_referrer",
  "confidence": { "kind": "deterministic" },
  "clickId": "clk_3Qm8xR2v",
  "payload": {
    "data": { "screen": "offer", "campaign": "summer" }
  }
}

The keys inside payload.data are yours to define, so keep the same key names you used in Branch's metadata and the switch statement or router map that consumes them needs zero edits. Only the two calls that fetch the payload change.

Keep the returned clickId. Send it when the user signs up or makes a purchase so the dashboard can connect the outcome to the link and campaign, not just count the open.

05

Cut over on your schedule, not a vendor's.

Unlike a shutdown migration, you can run both providers side by side. Point new campaign links at Ferry while existing Branch links keep resolving, and retire Branch once the last high-value link and the last SDK reference are gone.

An installed iOS app opens the intended screen from the link.

An installed Android app opens the intended screen from the link.

A device without the app reaches the correct App Store or Play Store page.

The first launch after install receives the expected payload.

A desktop visitor reaches the configured web fallback.

A signup or purchase appears against the right link and campaign.

For a custom domain, lower DNS TTL before the cutover if your provider allows it. Recreate the important slugs, verify the well-known files, ship the app update, and only then point the hostname at Ferry. Keep a rollback note until traffic and attribution look normal.

Once every link and SDK call has moved, remove the Branch SDK entirely and cancel the contract. That last step is the one that actually fixes the invoice.

ARRIVE

The goal is not link parity. It is a bill that matches what you use.

A successful migration leaves users with a working route, your app with the same routing code it already had, and your team paying for links and events instead of every session your app happens to open.