Skip to content
GuidesLinks into your app

Guides

Links into your app

Where a publisher’s link sends each phone, how the invite reaches your app on each platform, and what to set up: your store links, the code field, Google Play, app links, and optionally install matching, an App Clip or your own deep link.

A publisher shares a Flow Affiliate link, https://go.affiliate.yflow.io/a/<code>. Flow Affiliate records the click and sends the phone straight on: an iPhone to your app in the App Store, an Android phone to Google Play. There is no page in between. The Expo SDK, @flow/expo, catches whatever arrives in the app.

What arrives depends on the phone. Android is reliable: Google Play hands the link's referrer to your app after the install, with the click in it. Nothing passes through the App Store, so on iPhone the invite needs another way in:

  • An installed app opens straight from the link.
  • Install matching, if you turn it on, matches the app's first launch to a recent click from the same network and the same kind of phone, as mobile measurement partners do. It is a probable match, scored by how much agrees.
  • An App Clip, if you choose one, hands the click to your app.
  • The code field: the person types the publisher's code into your app. The code is the last part of the link, and publishers share it with the link. Keep the field whatever else you use.

Where each phone goes

Set this up on your program in the dashboard, under Tracking → Links. It shows where a click sends each phone, worked out by the same rules the link uses.

PhoneGoes toHow the sign-up is creditedReliability
App installed, iOS or AndroidOpens the app directlyThe link is a universal link or app link once you add your app IDs. The SDK reads it.Very high
Android, not installedGoogle PlayGoogle Play's install referrer, read at first launch.High, for installs from Google Play
iPhone, not installedThe App Store (the default)Nothing passes through the App Store: install matching if it is on (below), else the code the person types into your app's code field.As often as people enter the code
iPhone or Android, not installed, install matching onIts store, as aboveThe app's first launch is matched to a recent click from the same network and the same kind of phone. On Android only when Google Play gave no referrer.Probable: can miss, or pick the wrong click
iPhone, not installed, App Clip chosenYour App ClipThe App Clip opens with the click and leaves it for the full app, which reads it at first launch.High in Safari and Messages; test in-app browsers
Your own deep link (Branch, AppsFlyer, Adjust, your site)Only if you choose it, per platformTheir deferred deep link, which you forward as params.Depends on their matching, often probabilistic on iOS
Computer, app onlyA page with a QR codeThe page shows the same link as a QR code; scanned with the phone, it is the click. It also shows your store links and the code. The computer is not counted as a click.As the phone it is scanned with
Computer, with a websiteThe webYour landing page or web sign-up URL, with fg_click and fg_ref added.As your site keeps them

When conversions happen only in your app, as you chose when you created it, a link never leads to your website. All you need are your store links: under Tracking → Links, paste your App Store link and your Google Play link; the app IDs are read from them. The program can't go live without at least one of them. A phone whose store link is missing goes to your own deep link if you have set one, and otherwise gets a short "not available yet" page; Links flags the missing link. Everything else (the install match window, an App Clip, your own deep link, opening the installed app, the App Store provider token and a tracking host) waits under Advanced. An app with a website keeps sending computers, and phones with no store link, to the website.

On iPhone the App Store is always the default. The App Clip is used only when you choose it as the iPhone route; entering its bundle ID alone changes nothing. Your own deep link is never the default either: choose it for iOS, Android or both when you want your link provider to decide what happens to a phone without your app.

The SDK tries every way most reliable first and keeps the first one that names a code or a click:

  1. the link that opened the app;
  2. what your App Clip left (iOS);
  3. the Play install referrer (Android);
  4. your deep-link SDK's params, when you hand them over before the install match answers;
  5. the install match, when you have turned it on.

What none of these catches is left to the code field. The SDK runs once per install: the next launch answers with what it caught the first time. Whatever it catches comes out as referral, which is exactly the body of POST /v1/attributions: your app hands it to your backend, and your backend forwards it as it is.

Set up the SDK

The SDK is plain JavaScript and holds no keys. It needs expo-application (the Play referrer), expo-device (the OS version and model for install matching) and somewhere to keep the install's catch, such as AsyncStorage.

npx expo install expo-application expo-device @react-native-async-storage/async-storage

Make one catcher for the app and put the provider at the root. The provider starts the catcher when the app mounts. It reads links on go.affiliate.yflow.io and its subdomains, your app's link host among them, without being told. With a domain of your own, list it in linkHosts together with your app's link host, as Tracking → Links → Advanced → Tracking host shows it.

// affiliates.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createAffiliateCatcher } from '@flow/expo';

export const affiliates = createAffiliateCatcher({
  storage: AsyncStorage,
  // only with a domain of your own: your app's link host and that domain
  linkHosts: ['yourapp.go.affiliate.yflow.io', 'go.yourapp.se'],
});
// app/_layout.tsx
import { AffiliateProvider } from '@flow/expo';
import { Stack } from 'expo-router';
import { affiliates } from '../affiliates';

export default function RootLayout() {
  return (
    <AffiliateProvider catcher={affiliates}>
      <Stack />
    </AffiliateProvider>
  );
}

When something is caught, send it to your backend. Asking binds nothing, so do it as soon as you have it, before sign-up if you like:

import { useAffiliate } from '@flow/expo';

const { affiliate } = useAffiliate();
useEffect(() => {
  if (affiliate) void api.post('/affiliate/attribution', affiliate);
}, [affiliate]);

When nothing was caught, as after an install from the App Store, show the code field at sign-up. See the code field.

The code field

Every app needs a field where the person can enter the publisher's code. Nothing survives the App Store, so on iPhone, without install matching or an App Clip, the code is how sign-ups from a publisher's link are credited; with them, it catches what they miss. Show it at sign-up when referral is null, and fill it in when something was caught, so the person sees who invited them.

Your backend asks with the code. A Flow Affiliate code answers with an attribution key. Any other code answers unknown_code, so a field you already have for your own referral codes keeps working as before:

const ask = await fg.createAttribution({ code: form.referralCode, installId, platform: 'ios' });
if (ask.ok && !ask.data.attributable && ask.data.reason === 'unknown_code') {
  await ownReferralSystem(user, form.referralCode);
}

normalizeCode from @flow/expo reads what someone typed, also in groups (K7QX A2MP), or the code in a Flow Affiliate link pasted into the field. Codes are 3 to 24 letters and digits, and match whatever their case.

iPhone: the App Store

An iPhone that doesn't have your app goes straight to your App Store link. The click is recorded first, so publishers and you see it, but nothing about it reaches your app through the install. Install matching, if it is on, can tie the app's first launch to the click; otherwise the code field does the crediting.

Downloads per publisher in App Store Connect. Under Tracking → Links → Advanced, enter your App Store provider token: the number App Store Connect shows as pt in the campaign links it makes for your account. Flow Affiliate then adds Apple's campaign parameters to the App Store link: pt (the token), ct (the publisher's link code) and mt=8. App Store Connect's App Analytics then shows product page views and first-time downloads per campaign, which is per publisher's link. These figures are Apple's, shown only in App Store Connect, and count only people who share analytics with app developers. They are for your own reading and don't credit anyone: sign-ups are still credited by the link itself, the App Clip, the install match or the code.

An installed app opens straight from the link, on both platforms. Your app proves it owns the link's host, and the SDK reads the link it was opened with (Linking.getInitialURL() and the url event), so there is nothing else to call.

  1. Under Tracking → Links, fill in your iOS app IDs (Team ID and bundle ID), your Android package name and its SHA-256 certificate fingerprints. Flow Affiliate then publishes /.well-known/apple-app-site-association and /.well-known/assetlinks.json on your app's link host, covering /a/*. The page shows whether each file lists your app.
  2. Declare your app's link host in your app, as Tracking → Links → Advanced → Tracking host shows it, and your own domain too if you have one.
{
  "expo": {
    "ios": { "associatedDomains": ["applinks:yourapp.go.affiliate.yflow.io"] },
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [{ "scheme": "https", "host": "yourapp.go.affiliate.yflow.io", "pathPrefix": "/a/" }],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

With Expo Router, the link is also a route: add app/a/[code].tsx that redirects to your first screen, so the app doesn't show "Unmatched route".

Android: the Play install referrer

Paste your Google Play link under Tracking → Links. An Android phone without your app then goes straight to Google Play with referrer=fg_click=…&fg_ref=…. Google Play hands that string to your app after the install, and on the first launch the SDK reads it with getInstallReferrerAsync(). It carries the click itself, so nothing has to be typed.

Only installs from Google Play carry it: an app installed from another store, or sideloaded, has no referrer.

App Clip (optional)

An App Clip is a small part of your app that iOS opens without an install. It is never the default: choose it as the iPhone route when you have one. An iPhone that doesn't have your app then goes to Apple's App Clip link, https://appclip.apple.com/id?p=<App Clip bundle ID>, with fg_click and fg_ref added. iOS shows the App Clip card, the clip opens with the link, and it leaves the invite in storage it shares with your full app. When the person installs the full app, its first launch finds the invite there, with no code to type.

What to add:

  1. An App Clip target in your iOS project, with a bundle ID such as se.yourapp.app.Clip. With Expo, a config plugin can add the target; it runs the same React Native code as your app, so it can be a single screen.
  2. An App Group shared by the app and the clip, such as group.se.yourapp.app, in both targets' entitlements. The invite travels through it.
  3. The default App Clip experience in App Store Connect, with its header image, subtitle and action. Flow Affiliate uses Apple's default App Clip link, so you need no advanced experience and no website of your own.
  4. Two SDK calls: the clip saves the invite it was opened with, and the full app reads it.
// in the App Clip: catch the link it was opened with, then leave it for the full app
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createAffiliateCatcher, saveHandoff } from '@flow/expo';
import { groupStorage } from './group-storage'; // getItem / setItem over the shared App Group

const clip = createAffiliateCatcher({ storage: AsyncStorage });
void clip.start().then(({ affiliate }) => saveHandoff(affiliate, groupStorage));
// in the full app: read what the clip left, on the first launch
export const affiliates = createAffiliateCatcher({ storage: AsyncStorage, handoff: groupStorage });

groupStorage is anything with getItem(key) and setItem(key, value) that reads and writes the App Group, for example a shared-group preferences library wrapped in those two functions. What the clip left comes out as a referral with source: 'app_clip', as sure as a link open. It is kept for 30 days.

Then, under Tracking → Links → Advanced, enter the App Clip bundle ID and choose App Clip as the iPhone route. Do it only once the App Clip is live in the App Store; switch back to the App Store at any time.

Branch, AppsFlyer and Adjust

If you already use a deep-link SDK, hand its referring params to the catcher as they are. Flow Affiliate reads fg_ref and fg_click from their keys, and from a Flow Affiliate link among their values, such as Branch's ~referring_link when it is one. Your own keys are ignored: params such as { code: 'FRIEND1', '~feature': 'referral' } from your app's own referral links name no Flow Affiliate invite.

import branch from 'react-native-branch';
import { affiliates } from './affiliates';

branch.subscribe({
  onOpenComplete: ({ error, params }) => {
    if (!error && params) void affiliates.ingestParams(params);
  },
});

An organic open, whose params name no code or click, is ignored. For AppsFlyer pass the deep link's data object; for Adjust pass the deferred deep link as { deeplink }. Branch's +match_guaranteed is passed on as the match's confidence; on iOS, an install that wasn't opened from a link is often only a probable match.

For Branch links to carry the invite, enter a Branch link as Your own deep link under Tracking → Links, for example https://yourapp.app.link/invite, and choose it as the route for iOS, Android or both. Flow Affiliate adds fg_click and fg_ref to it as query parameters, which Branch passes on in the link's data. Check it once with a test install: the params your app receives should hold both.

Install matching

Optional, and off until you turn it on in the SDK. When nothing more certain came with the install, the SDK asks Flow Affiliate once, at the app's first launch, whether it matches a recent click on one of your links. Flow Affiliate compares what it noted at the click with what the app sends:

  • the network: a keyed hash of the IP address, never the address itself (an IPv6 address by its /64 prefix);
  • the platform and the OS version, major and minor (18.1);
  • the phone's model, screen size and language, when both the click and the app have them. In-app browsers such as Instagram's and Facebook's name the model and the screen; Safari and Chrome don't;
  • the time zone, when both have it.

The click must be on one of your app's links and inside the match window: 24 hours by default, up to 72 hours under Tracking → Links → Advanced. The most recent click that fits wins, and a click matches one install at most. On Android the SDK asks only when Google Play gave no referrer, since the referrer is surer.

On iPhone, the first launch can come from another network than the tap: Wi-Fi at the tap, mobile data at the launch. It still matches a click from the last 2 hours when that click named the phone's model and screen, and they, the OS version, the language and the time zone all agree.

Every match gets a score from 1 to 99, higher being surer. The same network starts at 50 and another network at 20. Each signal that agrees adds to it (OS version 10, model 15, screen 10, language 5, time zone 5), a different screen or time zone takes 10 away, a click in the last hour adds 5 (in the last 6 hours, 2), and each other click that could have been the one takes 5. The score comes back with the match. Publishers and you see the matched installs per link and per program.

Turn it on with your app's id in Flow Affiliate and the API's address. The id is a UUID and not a secret, so it is safe to ship in the app.

export const affiliates = createAffiliateCatcher({
  storage: AsyncStorage,
  installMatch: { appId: '6f1c2b3a-4d5e-4f60-8a7b-9c0d1e2f3a4b', apiUrl: 'https://api.flowgroup.se' },
});

The OS version and model come from expo-device, the screen size from React Native and the time zone from JavaScript. The language is the JavaScript locale unless you pass one, for example language: getLocales()[0]?.languageTag from expo-localization. If Flow Affiliate can't be reached, the SDK asks again on a later launch, up to 72 hours after the first. A match comes out as a referral with source: 'install_match', confidence: 'probable', the code and clickId, and publisherName for showing "Invited by Anna". Your backend forwards it to POST /v1/attributions as it is; the ask ignores publisherName. As with every other way in, the user is attributed, and anything paid, only once your backend confirms them with user.registered and the key.

A match is probable, never certain. It can miss, or pick the wrong click: people on the same Wi-Fi with the same kind of phone look alike, a VPN or iCloud Private Relay in Safari changes the address between the tap and the install, and an OS update in between changes the version. Keep the code field as the fallback.

Next

  • Attribution for apps: what your backend does with what the app caught, from the ask to revenue.
  • Expo SDK: every function and option of @flow/expo, including installMatch.