Tracking Users from Ads Campaigns in Flutter using Install Referrer & Deferred Deep Links



When you run paid ad campaigns (Google Ads, Meta / Facebook Ads), one of the most important questions is:

Which campaign brought this user?

In this article, we will see how to track users coming from ad campaigns in a Flutter app using:

  • Google Play Install Referrer (Android)
  • AES encrypted campaign payload
  • Deferred Deep Linking (Meta / Facebook)
  • Real production-ready Flutter code

Why Install Referrer is Important

When a user installs your app from Google Play through an advertisement, Google attaches a special query string called the install referrer.

This referrer can contain:

  • Campaign ID
  • Ad source (Google, Meta, Affiliate)
  • Custom deep link data
  • Encrypted attribution payload

The install referrer is available only once, immediately after installation.


High-Level Flow

User clicks Ad
   ↓
Play Store installs app
   ↓
Install Referrer attached
   ↓
App reads referrer on first launch
   ↓
Decrypt campaign payload
   ↓
Store attribution data

For Facebook / Meta ads, Deferred Deep Links are used if the app was not installed earlier.


1. Fetching Install Referrer (Android)

We start by fetching the install referrer using the Play Install Referrer API:


ReferrerDetails referrerDetails =
    await AndroidPlayInstallReferrer.installReferrer;

This provides:

  • installReferrer → Campaign data
  • installVersion
  • googlePlayInstantParam

2. Validating & Processing Referrer Data

We ensure the referrer is not empty before processing:


if (referrerDetails.installReferrer != null &&
    referrerDetails.installReferrer!.isNotEmpty) {

A real referrer may look like:

utm_source=meta
&utm_medium=cpc
&utm_content=%7B...encrypted_payload...%7D

3. Extracting utm_content from Referrer

The campaign payload is stored inside utm_content.


final regex = RegExp(r'(^|&|\?).*?utm_content=([^&]+)');
final match = regex.firstMatch(decodedUri);

Why utm_content?

  • Supports custom JSON
  • Survives Play Store redirects
  • Perfect for encrypted payloads

4. Encrypted Campaign Payload (Security First)

The campaign payload is AES-256-GCM encrypted to prevent tampering and leakage.

Payload structure before encryption:

{
  "source": {
    "data": "encrypted_hex_string",
    "nonce": "iv_hex"
  }
}

5. Decrypting the Campaign Data

Decryption uses AES-256-GCM:


final cipher = GCMBlockCipher(AESEngine())
  ..init(false, AEADParameters(
      KeyParameter(Uint8List.fromList(aesKey)),
      128,
      Uint8List.fromList(iv),
      Uint8List(0)
  ));

final plaintext = cipher.process(ciphertext);

This ensures:

  • AES-256 encryption
  • Authenticated decryption
  • Nonce-based security

Final decrypted output:

{
  "campaign": "diwali_offer",
  "adset": "interest_trading",
  "creative": "video_1"
}

6. Storing Attribution Data

Once decrypted, attribution data is stored globally:


AppConstants.installReferrer = result;

This data can be used for:

  • Analytics
  • Custom onboarding
  • Feature unlocking
  • Revenue attribution

7. Deferred Deep Linking (Meta / Facebook)

Facebook ads require Deferred Deep Links:


String? link = await FlutterFacebookAppLinks.initFBLinks();

For iOS:


final iosLink = await FlutterFacebookAppLinks.getDeepLink();

This ensures campaign attribution even after app installation.


8. Handling Deep Link Routes

Example deep link:

myapp://dl?campaign=ipo_offer

You can:

  • Resolve short links
  • Extract query parameters
  • Navigate user to a specific screen

Final Architecture Overview

Ads Platform
   ↓
Encrypted Campaign Payload
   ↓
Play Store / Meta
   ↓
Install Referrer / Deferred Deep Link
   ↓
Flutter App
   ↓
Decrypt → Store → Use

Key Takeaways

  • Works without backend
  • Secure encrypted attribution
  • Supports Google & Meta Ads
  • Handles fresh installs
  • Production-ready approach

When Should You Use This?

  • Paid ad campaigns
  • Performance marketing
  • App install attribution
  • Feature unlock after install
  • Custom onboarding flows

This approach provides reliable, secure, and scalable campaign tracking for modern Flutter applications.

Complete code

Future<void> initReferrerDetails() async {
  print("DEEPLINK INSIDE initReferrerDetails");
  String referrerDetailsString;

  try {
    ReferrerDetails referrerDetails =
        await AndroidPlayInstallReferrer.installReferrer;

    referrerDetailsString = referrerDetails.toString();

    print("DEEPLINK INSIDE initReferrerDetails $referrerDetailsString");
    print("DEEPLINK INSIDE googlePlayInstantParam -- ${referrerDetails.googlePlayInstantParam}");
    print("DEEPLINK INSIDE installReferrer -- ${referrerDetails.installReferrer} -- ${referrerDetails.installVersion} -- ");
    print("DEEPLINK INSIDE installVersion -- ${referrerDetails.installVersion} -- ");

    if (referrerDetails.installReferrer != null &&
        referrerDetails.installReferrer!.isNotEmpty) {

      print("DEEPLINK INSIDE installReferrer is not Empty");

      final result = decryptMetaInstallReferrer(
        rawReferrer: referrerDetails.installReferrer!,
        keyHex: AppConstants.metaKeyHex,
      );

      AppConstants.installReferrer =
          result ?? referrerDetailsString ?? '';

      if (referrerDetails.installReferrer?.contains('dl') ?? false) {
        //TODO: deeplink redirection
      }

      print('Decrypted: $result ---- ${AppConstants.installReferrer}');
    }
  } catch (e) {
    referrerDetailsString =
        'DEEPLINK INSIDE Failed to get referrer details: $e';
  }
}

String? decryptMetaInstallReferrer({
  required String rawReferrer,
  required String keyHex,
}) {
  try {
    final decodedUri = Uri.decodeFull(rawReferrer);

    final regex = RegExp(r'(^|&|\?).*?utm_content=([^&]+)');
    final match = regex.firstMatch(decodedUri);

    if (match == null) {
      throw const FormatException('utm_content not found');
    }

    final utm = match.group(2)!;
    final jsonString = Uri.decodeFull(utm);

    print('Final JSON:\n$jsonString');

    final dataMap = jsonDecode(jsonString) as Map<String, dynamic>;

    final src = dataMap['source'] as Map<String, dynamic>;
    final encHex = src['data'] as String;
    final nonceHex = src['nonce'] as String;

    final aesKey = hex.decode(keyHex);
    final ciphertext = hex.decode(encHex);
    final iv = hex.decode(nonceHex);

    final cipher = GCMBlockCipher(AESEngine())
      ..init(
        false,
        AEADParameters(
          KeyParameter(Uint8List.fromList(aesKey)),
          128,
          Uint8List.fromList(iv),
          Uint8List(0),
        ),
      );

    final plaintext =
        cipher.process(Uint8List.fromList(ciphertext));

    return utf8.decode(plaintext);
  } catch (e) {
    print('Decryption error: $e');
    return null;
  }
}

Future<void> fetchDeferredDeepLink() async {
  print("DEEPLINK _fetchDeferredDeepLink");

  try {
    String? link =
        await FlutterFacebookAppLinks.initFBLinks();

    if (Platform.isIOS) {
      final iosLink =
      
          await FlutterFacebookAppLinks.getDeepLink();
      if (iosLink.isNotEmpty) {
        link = iosLink;
      }
    }

    if (link.toString().contains("/dl/")) {
      //TODO: deeplink redirection
    }

    print("_fetchDeferredDeepLink $link");
  } catch (e) {
    print('Facebook deferred deep link error: $e');
  }
}

Post a Comment

Previous Post Next Post

Subscribe Us


Get tutorials, Flutter news and other exclusive content delivered to your inbox. Join 1000+ growth-oriented Flutter developers subscribed to the newsletter

100% value, 0% spam. Unsubscribe anytime