Skip to content
GuidesAttribution for apps

Guides

Attribution for apps

Three calls from your backend: ask about what your app caught, confirm the user once you know they are new, then report what they do with the key. Every call is signed and server to server.

Your app knows who is new. Flow Affiliate knows which link or code brought them, and what that earns. An app integrates in three calls, all from your backend, signed with your app key:

  • Ask with POST /v1/attributions as soon as the app has caught something. The answer comes at once, with an attribution key.
  • Confirm with user.registered carrying the key, once you know the user is new.
  • Report every later event with the key.
  1. 1Your app
    First open: a link, the App Clip, Branch params, the Play referrer or an install match; or a code typed at sign-up
  2. Your app → Your backend
    What it caught, as it caught it, with an install id
  3. 2Your backend → Flow Affiliate
    Ask: POST /v1/attributions
  4. Flow Affiliate → Your backend
    Yes with an attributionKey, or no with a reason
  5. 3Your backend → Flow Affiliate
    Confirm: user.registered with the key, once the user is new
  6. 4Your backend → Flow Affiliate
    Report: every later event, with the key

Three rules keep the integration small:

  • Ask as early as you have something. Confirm when you know. Asking binds nothing and pays nothing; only the confirmation does.
  • Send each meaningful event once, with its money fields. Flow Affiliate decides what pays, so changing an offer never needs a code change on your side.
  • Only send events for users who have a key. Flow Affiliate never needs data about anyone else.

The examples use the Node SDK, or this shell helper, which signs a body and sends it:

fg() { # fg POST /v1/events '<json>'
  local ts sig
  ts=$(date +%s)
  sig=$(printf '%s.%s' "$ts" "$3" | openssl dgst -sha256 -hmac "$FG_SECRET" | sed 's/^.*= //')
  curl -s -X "$1" "https://api.flowgroup.se$2" -H "content-type: application/json" \
    -H "x-fg-key: $FG_KEY_ID" -H "x-fg-timestamp: $ts" -H "x-fg-signature: $sig" --data-raw "$3"
}
  1. Flow Affiliate never attributes an install on its own. Your app catches what it was installed or opened with and hands it to your backend as it is, with no mapping code. In an Expo app, @flow/expo does the catching:

    import AsyncStorage from '@react-native-async-storage/async-storage';
    import { createAffiliateCatcher, AffiliateProvider, useAffiliate } from '@flow/expo';
    
    export const affiliates = createAffiliateCatcher({ storage: AsyncStorage });
    // at the root: <AffiliateProvider catcher={affiliates}>…</AffiliateProvider>
    
    const { affiliate } = useAffiliate();
    useEffect(() => {
      // exactly the body of POST /v1/attributions: your backend forwards it as it is
      if (affiliate) void api.post('/affiliate/attribution', affiliate);
    }, [affiliate]);

    It tries every way the code can travel, most reliable first, and keeps the first catch of the install:

    • the link that opened the app, as url: a universal link or app link carrying fg_click and fg_ref, or a Flow Affiliate link https://go.affiliate.yflow.io/a/<code>;
    • on iOS, what your App Clip left for the full app, as code and clickId;
    • the referring params your deep-link SDK gives you (Branch, AppsFlyer, Adjust), handed to ingestParams and sent as params: the whole object. Flow Affiliate finds fg_ref and fg_click in its keys and in a Flow Affiliate link among its values, such as Branch's ~referring_link;
    • the Play install referrer on Android, read at first launch, as code and clickId;
    • with install matching on, a probable match of the first launch to a recent click from the same network and kind of phone (on iPhone also from another network, when the phone agrees in every signal), as code and clickId with source: 'install_match' and confidence: 'probable'.

    What none of these catches, as an iPhone install from the App Store often is, is left to the code field: the person types the publisher's code, and your backend asks with it as code.

    Flow Affiliate links and data are separate from your app's own referral system: only fg_click, fg_ref and Flow Affiliate links are read, never your own keys such as code. Catching the invite works every time on Android with Google Play. On iPhone an installed app opened from the link, or an App Clip if you choose one, carries the click; install matching makes a probable match; the code field catches the rest. Links into your app shows how the invite travels on each platform and what to set up for each way.

    The SDK also makes a random install id at first launch and keeps it on the device. Sent with the ask, it makes asking again from the same install safe: the answer is the same key. Without the SDK, send the same fields yourself.

    The secret never goes into the app. The app talks to your backend; your backend talks to Flow Affiliate.

  2. Ask as soon as your backend has something, before sign-up if you like. The answer comes at once, so you can show "Invited by Anna" on the sign-up screen.

    fg POST /v1/attributions '{"params":{"+clicked_branch_link":true,"+match_guaranteed":false,"fg_ref":"k7qxa2mp","~referring_link":"https://example.app.link/AbC123"},"installId":"5b1d0c9e-2f4a-4b8c-9d7e-6f5a4b3c2d1e","platform":"ios"}'
    # {"attributable":true,"attributionKey":"fga_k7qxa2mpw4ht9bvc3rnd8ezj","status":"pending",
    #  "partner":{"displayName":"Anna Berg"},"program":{"id":"…","name":"Invite friends"},
    #  "inviteeBonus":null,"expiresAt":"2026-12-09T09:30:00.000Z"}

    An install match is asked about the same way, as it came: { "code": "k7qxa2mp", "clickId": "…", "source": "install_match", "confidence": "probable", "installId": "…" }. Its publisherName is for your sign-up screen and is ignored here. A probable match is attributed and paid like any other once you confirm the user; the confidence is recorded. See install matching.

    When nothing was caught, ask with the code the person typed at sign-up: { "code": "k7qxa2mp", "installId": "…" }. See the code field.

    A yes carries the attributionKey. Keep it with the install or the sign-up session until you know whether the user is new. It expires after the program's attribution window (expiresAt). inviteeBonus is the welcome gift the link carries for the new user, when it carries one.

    A no is an answer too, with status 200 and a reason:

    no_affiliatenothing to go on
    Nothing you sent names a code or a click, as for an install that came from no link.
    unknown_codenot yours
    No code like this belongs to your app. A typed code may belong to your own referral scheme instead.
    unknown_clicknot yours
    No code was sent, and the click id is not a click on one of your links.
    code_inactiveswitched off
    The code was switched off or has expired, or its publisher can no longer be paid.
    program_closedclosed
    The program the code belongs to is not running.
    already_attributedtaken
    Only when you sent appUserId: that user is already attributed.
  3. Whether the user is new is your decision, by your own rules: someone who had an account before, or reinstalls, is not new. Once you know, send user.registered with the key and the new appUserId. That binds the key to the user and attributes them to that referral, whatever else is known about them, and pays the sign-up rule if the offer has one.

    fg POST /v1/events '{"eventId":"registered:u_1042","type":"user.registered","occurredAt":"2026-11-09T09:30:00Z","appUserId":"u_1042","attributionKey":"fga_k7qxa2mpw4ht9bvc3rnd8ezj"}'
    # {"accepted":[{"eventId":"registered:u_1042","status":"accepted"}]}

    If the user is not new, don't send the key. It expires unused. If you want the publisher to see why rather than a silent expiry, decline it: POST /v1/attributions/<key>/decline with a reason (existing_user, duplicate, suspected_fraud or other).

    An event whose key can't be used is not stored. It comes back rejected with a reason, so you can fix it and send the same eventId again:

    unknown_key
    No key like it belongs to your app in this environment.
    key_expired
    The key was never confirmed, and its window has closed.
    key_bound_to_other_user
    The key was confirmed for another appUserId.
    key_not_registered
    The key is not confirmed yet: only user.registered with an appUserId binds it.
    key_declined
    You declined the key.
  4. Every later event carries the key. appUserId may come along too, or be left out: the key stands for the user it was confirmed for. The type is a standard event or one of your own from the app's event catalog, such as level.completed in a game. The offer's rules decide what pays:

    • a once-only rule pays on the first matching event. Later events of that type are counted and shown in the event feed, but pay nothing more;
    • a recurring rule pays on every matching event.
    fg POST /v1/events '{"eventId":"level:u_8F2K:12","type":"level.completed","occurredAt":"2026-11-10T14:02:00Z","attributionKey":"fga_k7qxa2mpw4ht9bvc3rnd8ezj","metadata":{"level":12}}'
  5. For purchases, subscriptions or what the user earns in your app, send amount in minor units (cents) and currency. Amounts add up per user and publisher, and pay only through a revenue-share rule: it pays its percentage of each amount, and a milestone rule pays once the user's total reaches a threshold. An amount in another currency than the rule's pays nothing.

    fg POST /v1/events '{"eventId":"order:o-77120","type":"purchase.completed","occurredAt":"2026-11-12T08:00:00Z","attributionKey":"fga_k7qxa2mpw4ht9bvc3rnd8ezj","amount":1999,"currency":"USD"}'
  6. Use your own transaction id as eventId: registered:<user id>, order:<order id>. Flow Affiliate deduplicates on app, eventId and environment, so sending an event again answers duplicate and changes nothing. Retry any timeout or 5xx with the same eventId.

    To undo something (a refund, a cancelled subscription), send an event with reversesEventId set to the original's eventId. See reversals.

Example: Branch at first open

Here is how an app that uses Branch does it. The app gets Branch's params at first open and posts them to the backend after login. The backend asks with the params and the install id, then confirms straight away, because it knows the user registered a moment ago. Nothing is held back waiting for the attribution to settle.

Ask and confirm after sign-up
// first open: keep what Branch hands over, as it is
branch.subscribe(({ params }) => {
  if (params?.['+is_first_session']) storage.set('referringParams', JSON.stringify(params));
});

// after sign-up or login: hand it to the backend once
const params = storage.get('referringParams');
if (params) {
  await api.post('/affiliate/attribution', { params: JSON.parse(params), installId: storage.get('installId') });
  storage.delete('referringParams');
}

Optionally, a code typed into your own referral code field can be checked the same way. Ask with the code; when it is not a Flow Affiliate code, your own referral scheme handles it as before:

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

From then on every event goes out once, with the key and its money fields:

await fg.track({
  type: 'purchase.completed',
  eventId: `order:${order.id}`,
  attributionKey: user.attributionKey,
  amount: order.total,
  currency: 'USD',
});

Other ways in

Both earlier paths still work:

  • Claim a code for a user who already has an account: POST /v1/affiliates/claim with the appUserId and the code. A registration that arrived earlier without a code is attributed again when the claim lands. The answer carries the claim's attributionKey.
  • One call: send user.registered with affiliateCode and/or clickId and no key. Simpler, but there is no answer to show: an unknown code leaves the user unattributed (no_touch in the event feed).