Developer guide · v1.2 · Interactive

AirBridge → Mixpanel Instrumentation Mapping

Owner Sourabh Mishra (MMRP) Route Sourabh → Kapil → Prajwalit Audience iOS + Android
Sink: Mixpanel only Prod 4017469 Staging 4017466 Dev 4002103 Prod is behind — see §Naming v1.2 adds marketing attribution (§3b) — see changelog at bottom

Click any link parameter to see where it lands

This is a real example AirBridge link. Every parameter maps to something in Mixpanel — or explicitly doesn't. Click one to see the rule.

Example link

Also part of the mapping — not URL query params
Maps to Mixpanel Not forwarded — debug only

Property-naming rule — do not drift

All optional link-derived event props use the exact mx_* names from the param_map in mixxi_attribution_enums.json: mx_channel, mx_placement, mx_phase, mx_episode, mx_recipe, mx_asset, mx_cta, mx_team, mx_funding, mx_ad_platform, mx_persona, mx_layer, mx_aff. MMRP's Channels tab and readout query these exact names — a prop written under any other name will exist in Mixpanel but be invisible to every MMRP surface. The three profile keys keep their own names: acquisition_source, marketing_affiliate_id, airbridge_id.

Touch semantics: profile vs. super props

The rule that's easy to get backwards: profile fields never change after first touch, but the mx_* super props update on every new tagged open — and organic opens don't clear them. Step through three sessions for the same user to see it happen.

Play through: one user, three sessions

The chain — what happens on a click

The app owns everything inside the device. The server-side join is MMRP's — but it only works if the app sets the join keys, which is why this is a developer spec.

User
Taps a go.mixxi.ai / abr.ge link (carries channel, campaign, sub-params).
AirBridge → OS
Redirect + store fallback, then the OS deep-link resolver opens the app (or triggers install → first launch).
App — consent gate
DPDP consent gate. Nothing fires until affirmative consent.
App — SDK init order
Mixpanel initializes first (distinct_id ready), then AirBridge SDK initializes.
AirBridge → App
Deferred deep-link params on first launch, or immediate params if already installed.
App → Mixpanel
Sets acquisition_source ($set_once), marketing_affiliate_id, airbridge_id.
App → Mixpanel
Tracks signup_complete / live_watch_anchor with join props attached.
AirBridge → MMRP
Server postback carries deterministic attribution data.
MMRP
Joins the postback to Mixpanel via airbridge_id / marketing_affiliate_id.

Identity & timing model

Four rules, in this order. Breaking the order causes silent attribution loss — no error, just a missing join.

  1. Consent gate is absolute (DPDP). No SDK initializes, no token/cookie/storage is written, and no event fires before affirmative consent. Deny-by-default.

  2. Mixpanel initializes before any attribution event. If an attribution event fires before Mixpanel has a distinct_id, the event is lost and never attributed. Order: consent → Mixpanel init → AirBridge init → read link → set props.

  3. Read the deferred deep link on first launch. For a new install, the affiliate code arrives via AirBridge's deferred deep-link callback — not the immediate handler. Handling only immediate deep links makes every affiliate install look organic, and no affiliate gets paid. Handle both.

  4. acquisition_source is first-touch immutable. Write it with $set_once — never overwrite. Null resolves to unknown. marketing_affiliate_id and airbridge_id are set on first attributed touch and used as deterministic join keys — no fingerprinting.

Step-by-step (app side)

Illustrative pseudo-code — use the real AirBridge/Mixpanel SDK method names per platform. The order and the property names are what matter.

// 1. Consent gate — DPDP deny-by-default. Nothing below runs until true.
if (!userHasConsented()) return;

// 2. Mixpanel FIRST — distinct_id must exist before any attribution event.
mixpanel.init(MIXPANEL_TOKEN);            // token per environment
mixpanel.identify(stableDistinctId());

// 3. AirBridge SDK.
airbridge.init(AIRBRIDGE_APP_TOKEN);      // token per environment — never staging token in prod build

// 4. Deferred deep link (NEW installs) + immediate (existing installs).
airbridge.onDeferredDeeplink(params -> applyAttribution(params));
airbridge.onDeeplink(params -> applyAttribution(params));

function applyAttribution(params) {
    // 5a. PROFILE — first-touch, immutable (never overwrite).
    mixpanel.people.setOnce({ acquisition_source: rollup(params.channel) });  // see §3 Pin #0 — raw channel until rollup ships
    mixpanel.people.set({ airbridge_id: airbridge.getAirbridgeId() });

    // affiliate links only — code carried in `campaign`, matches deep-link path
    if (isAffiliate(params)) {
        mixpanel.people.set({ marketing_affiliate_id: params.campaign });     // first attributed touch
    }

    // 5b. MARKETING CONTEXT — latest-touch super props (§3b). OVERWRITE on every
    // attributed open; ride automatically on all subsequent events.
    mixpanel.register({
        acquisition_source: rollup(params.channel),
        mx_channel:   params.channel,
        mx_phase:     splitCampaign(params.campaign).phase,      // marketing links: phase__episode
        mx_episode:   splitCampaign(params.campaign).episode,
        mx_recipe:    splitCreative(params.ad_creative).recipe,
        mx_asset:     splitCreative(params.ad_creative).asset,
        mx_placement: params.content,
        mx_funding:   params.sub_param_funding,                  // organic | sponsorship_funded
        mx_ad_platform: params.sub_param_ad_platform,            // paid only; omit when absent/none
        mx_persona:   params.sub_param_persona,
        mx_layer:     params.sub_param_layer,
        mx_team:      params.sub_param_team,
        mx_cta:       params.sub_param_cta,
        ...(isAffiliate(params) && { marketing_affiliate_id: params.campaign, mx_aff: params.campaign })
    });  // omit any absent param entirely — no empty strings
}

// 6. Emit funnel events with join props attached (super props ride automatically).
mixpanel.track("signup_complete", { new_otp_unique_account: <bool> });         // NOT account_created
mixpanel.track("live_watch_anchor", { episode_id, episode_number, watch_duration });

Platform notes

  • iOS: register deep-link handling in the SceneDelegate/AppDelegate path AirBridge documents; ensure ATT/consent timing does not fire attribution before Mixpanel init.
  • Android: intent filters + autoVerify; ensure the deferred-link callback is registered before the first activity that could emit an event.
  • Both: enum values are lowercase; Mixpanel will not dedupe WhatsApp vs whatsapp.

Server-side join — why the app props matter

The AirBridge server postback hits MMRP at /api/ingestion/airbridge/*. MMRP joins that postback to the Mixpanel funnel using airbridge_id (and marketing_affiliate_id for affiliate credit). The join is deterministic-only — if the app didn't set these keys, the postback and the funnel can't be joined and the install looks organic.

Affiliate credit follows a no-clawback ledger: money is only released on met conditions, never reversed — so a missing join key means an affiliate is silently underpaid. That's why §2/§3 are strict.

Naming & parity

DPDP hard constraints

Verify — one decisive end-to-end test

Do this on a clean device (no prior install), per platform, per environment.

  1. Cut a test link (go.mixxi.ai or abr.ge) with a known campaign code, e.g. RISING-E0-TESTONLY, deep link mixxi://mixxi/m/RISING-E0-TESTONLY.
  2. Tap it → install → open → complete consent → sign up.
  3. In Mixpanel Live View (correct environment), confirm: signup_complete fired; marketing_affiliate_id == RISING-E0-TESTONLY exactly; acquisition_source set (not null/unknown for a known channel); airbridge_id present.
  4. Confirm the AirBridge postback for the same install carries the same code, and MMRP resolves the join.

Success criteria

The same affiliate code is visible on both sides (AirBridge postback and Mixpanel marketing_affiliate_id) and MMRP joins them. If the code is missing on the Mixpanel side, the break is app-side (deferred link not read, or props set before Mixpanel init). If present in Mixpanel but not joined, the break is the postback field mapping (see Open Pins).

Second decisive test — a marketing link (new in v1.2, do this too)

  1. Cut a marketing test link: channel instagram, campaign phase1_tease__ep01_discovery, creative trend__testasset_01, sub_param_funding=organic.
  2. Clean device → tap → install → consent → sign up → then trigger one more event (e.g. open an episode).
  3. In Mixpanel Live View confirm: signup_complete AND the later event both carry mx_channel=instagram, mx_phase=phase1_tease, mx_episode=ep01_discovery, mx_recipe=trend, mx_asset=testasset_01, mx_funding=organic — and no marketing_affiliate_id (it's not an affiliate link).
  4. Latest-touch check: on the same device, tap a second marketing link with a different channel (e.g. whatsapp) → open → trigger an event → confirm the event's mx_channel is now whatsapp while the profile's acquisition_source is unchanged from the first link.

Success criteria

Funnel events carry the full mx_* context matching the link that drove the session; the profile stays first-touch. If events show no mx_* props, the super-prop registration isn't firing. If they show the old campaign after a new tagged open, the overwrite semantics are wrong (§3b — see the Touch semantics walkthrough above).

Open pins

New in v1.1 — Owner: Sourabh/MMRP, ships as enums v1.4.0

Pin #0 — channel → acquisition_source rollup enum. Does not yet exist in the committed enums file. Until it ships: write the raw channel value into acquisition_source ($set_once, null→unknown) rather than guessing a coarse mapping — a wrong value written $set_once is permanent.

Pending — Punit (template owner)

Affiliate-code postback field — partner vs sub_id. Which AirBridge field the tracking template forwards to the MMRP receiver is console-defined and unconfirmed. Build against campaign as the in-app source of marketing_affiliate_id (that's stable); treat the postback field name as unconfirmed. Recommendation on record: forward as a dedicated sub_id, not partner.

In progress — parallel workstream

Branded domain go.mixxi.ai. Being stood up in parallel (CNAME pending DNS). Does not change the affiliate-code parameter contract. Deferred-deep-link attribution works regardless of domain; Universal Link / App Link direct-open on the new domain is a separate, freeze-gated entitlement change.

Updated in v1.1 — ownership: Unravel

vote_cast source — client Mixpanel vs. server-side emit from the Kafka rising.vote.validated stream. The Kafka topic is now Unravel's (transferred with the Void gap). Preferred resolution on record: Unravel's backend consumes the validated stream and emits vote_cast server-side into Mixpanel (keeps votes trusted for payout release and keeps MMRP Mixpanel-only). Do not build vote_cast client-side until settled.

New in v1.1 — MMRP action item

Pin #4 — signup_complete as the payout event. This guide standardises on signup_complete (the only signup event that exists in any environment). MAP payout logic historically referenced account_created; re-pointing it to signup_complete is an MMRP action item, tracked separately. App-side: emit signup_complete, never account_created.

Changelog — what changed

Two revisions today. v1.1 corrected claims that turned out to be wrong against the committed enums file. v1.2 is additive: a whole new attribution contract for non-affiliate marketing links that didn't exist before.

v1.1 — corrections against the committed enums file

Fixed a fabricated claim. v1.0 said the acquisition_source rollup "lives in mixxi_attribution_enums.json." It doesn't — no such group exists in the committed file. Now an explicit open pin with a safe interim (write the raw channel value).

Fixed funding values. organic/paidorganic/sponsorship_funded — the only two values that exist in the enum.

Fixed event-prop names to the committed param_map. persona/funding_type/distribution_layer/owner_team/ctamx_persona/mx_funding/mx_layer/mx_team/mx_cta. MMRP's dashboards query the mx_* names — the v1.0 names would land in Mixpanel but be invisible to every MMRP surface.

Added the missing sub_param_ad_platformmx_ad_platform row (paid links only — present when funding=sponsorship_funded, omitted for organic).

Updated the vote_cast pin with Unravel ownership and the preferred server-side-emit resolution; added a new pin on the signup_complete payout re-point.

v1.2 — marketing attribution added (new, not a correction)

A third load-bearing output. Alongside acquisition_source and marketing_affiliate_id, every session must now carry a full set of mx_* campaign-context super props — mx_channel, mx_phase, mx_episode, mx_recipe, mx_asset, mx_placement, mx_aff. This is what feeds the Channels tab, the Saturday readout, and per-creative analysis. Previously about half the param_map was unmapped to anything.

Touch semantics — the new hard part. Profile fields (acquisition_source, marketing_affiliate_id) stay first-touch immutable, but the mx_* super props are latest-touch — overwritten on every new tagged open, left alone on organic opens. Getting this backwards (e.g. $set_once-ing the super props, or clearing them on organic opens) silently breaks channel/creative attribution. See the interactive walkthrough above.

A second verify test (§8) specifically for marketing links, including a "tap a second link, different channel" step to confirm the overwrite behavior actually happens.