Webclat logoWebclat . | Amplitude Solutions

QA - identity & attribution

How do I merge Branch.io/Segment attribution data with Amplitude's own user identity so it's not double-counted?

The fix, why it happens, and how to confirm it actually worked in your own instance.

The fix, in short

Branch, Segment, and Amplitude each maintain their own idea of "who this user is" - Branch by its own device fingerprint and deep-link data, Segment by its anonymousId/userId pair, Amplitude by its own device ID and user ID - and if you never explicitly reconcile them, the same real person can appear as two or three separate profiles in Amplitude. The fix is to designate one identifier (almost always your own authenticated user ID) as the source of truth and pass it into all three systems at the exact same login moment.

Why this happens

Attribution tools like Branch exist specifically to bridge a gap Amplitude doesn't solve on its own: linking an ad click or deep link, which happens before your app is even installed, to the first session inside the app. Branch does this with its own matching logic (deferred deep linking, device fingerprinting) and hands your app a set of attribution data on first open - but that data arrives tagged to Branch's own identifiers, not Amplitude's device ID, because at that point Amplitude's SDK may not have initialized yet.

Segment sits in a different spot: as a customer data platform, it often runs its own Amplitude integration (a "destination") that translates Segment's `identify()`/`track()` calls into Amplitude API calls on your behalf. If your app also calls Amplitude's own SDK directly - a common pattern when Segment was added later without removing the original direct integration - you now have two independent paths writing into the same Amplitude project, each potentially assigning its own device ID to the same physical device.

The double-counting happens because none of these three systems automatically knows the other two exist. Each one is internally consistent with itself; the disagreement only becomes visible in Amplitude, because Amplitude is the one place all three data flows converge, and Amplitude has no way to know that device ID A (from its own SDK), the Segment anonymousId B, and the Branch-attributed identity C are actually the same living, breathing person unless something tells it so explicitly.

Fix it

If Segment is your primary integration path

  1. Turn off Amplitude's own SDK-level device ID generation as an independent source of truth where Segment's Amplitude destination is active - decide which one owns identity and stick to it consistently across every page/screen, rather than letting both run in parallel.
  2. Call `analytics.identify(userId)` (Segment's call) at the same login moment you would otherwise call Amplitude's `setUserId()` directly - Segment's destination forwards this to Amplitude as the authoritative user ID, which is what actually merges previously anonymous sessions into the known user's profile.
  3. Audit for any direct `amplitude.track()` or `amplitude.identify()` calls still living in your codebase alongside the Segment integration - a leftover direct call from before Segment was added is the most common reason two identity streams exist when only one was intended.

If Branch is your attribution source

  1. On first app open, retrieve Branch's attribution/deep-link data from its init callback before or immediately alongside Amplitude's own init - Branch's SDK is generally designed to resolve this early specifically so it can be attached to the very first tracked events.
  2. Set the resulting campaign/channel/referrer values as Amplitude user properties (via an Identify object) on the same user profile that Amplitude's own device ID will track going forward - don't create a second Amplitude user for the attribution data; attach it to the one identity.
  3. Once the user authenticates (signs up or logs in), call Amplitude's `setUserId()` as normal - this merges the anonymous, attribution-tagged device profile into the authenticated user identity, so the Branch-sourced campaign data survives the anonymous-to-known transition instead of being orphaned on a device ID nobody looks up again.

How to verify it worked

  1. Pick one test device, walk it through the full flow deliberately: a Branch-attributed link click, first app open, and login - then check Amplitude's User Look-Up for that user and confirm exactly one merged profile exists, carrying both the attribution properties and the post-login events, not two separate profiles.
  2. In Amplitude's user-merge history (visible on a user's profile page when identities have been merged via setUserId) confirm the anonymous pre-login device ID and the post-login user ID show up as merged, not as two independent, unconnected entries.
  3. Compare a total-user count in Amplitude against your own source-of-truth user count (your database's registered-user count) for the same date range - a gap significantly larger than expected anonymous-only traffic suggests identities are still fragmenting somewhere in the chain.
  4. If Segment is involved, check Segment's own debugger for the test session and confirm only one identify call reaches Amplitude's destination per login - two calls per login (one from Segment, one from a leftover direct SDK call) is the signature of the duplicate-path problem this fix addresses.

Related: Why does Amplitude's device ID regenerate on every session/tab instead of persisting? · Why do my Amplitude revenue properties ($price/$quantity/$revenue) get set to zero after sending?

Frequently asked questions

Which identifier should actually win if Branch, Segment, and my own login system all disagree?+

Your own authenticated user ID, because it's the one identity your business already treats as ground truth for billing, support, and account data - Branch and Segment identifiers exist to route data toward that identity, not to replace it. Design the integration so both feed into your user ID rather than proposing an alternative one.

Is it safe to remove one of the two integration paths (direct SDK vs. Segment) once this is fixed, or should both stay?+

Running both long-term is rarely worth the ongoing reconciliation risk unless each is deliberately responsible for a distinct, non-overlapping set of events - for most teams, picking one path as the single source of truth for Amplitude and decommissioning the other is more maintainable than keeping the merge logic correct forever.

Still stuck, or want it checked properly?

The free scored audit reads your Amplitude instance across ten dimensions - taxonomy health, identity resolution, revenue tracking, and more - and hands back the three highest-payoff fixes. No commitment, 48-hour turnaround.

Request the free audit