Skip to content

Flutter SDK ​

Complete guide for integrating Linkrunner in Flutter apps

Prefer to let your AI coding agent do this? Install the Linkrunner skill and Claude Code, Cursor, GitHub Copilot, or Windsurf will wire up the SDK and deep links for you:

bash
npx @linkrunner/skills add flutter

Then ask your agent to "integrate Linkrunner". See Linkrunner Agent Skills.

Watch the setup video

Installation ​

Requirements ​

  • Flutter 3.19.0 or higher
  • Dart 3.3.0 or higher
  • iOS 15.0+ / Android 5.0 (API level 21) and above

Step 1: Add the Package ​

Run the following command to add the latest version of the Linkrunner package to your project:

bash
flutter pub add linkrunner

This command will automatically:

  • Add the latest version of linkrunner to your pubspec.yaml
  • Download and install the package and its dependencies

Step 2: Platform Specific Setup ​

Android Configuration ​

  1. Ensure your project's minSdkVersion is at least 21 in your android/app/build.gradle file:
gradle
android {
    defaultConfig {
        minSdkVersion 21
        // other config...
    }
}
  1. Add the following permissions to your AndroidManifest.xml file:
xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Note: The AD_ID permission (<uses-permission android:name="com.google.android.gms.permission.AD_ID" />) is already included in the SDK and is required for collecting device identifiers (GAID). If your app participates in Designed for Families, you should revoke AAID and disable AAID collection. See the Disabling AAID Collection section for more details.

Revoking the AD_ID Permission ​

According to Google's Policy, apps that target children must not transmit the Advertising ID.

To revoke the AD_ID permission, use Flutter SDK version 3.5.0 and above. Children apps targeting Android 13 (API 33) and above must prevent the permission from getting merged into their app by adding a revoke declaration to their Manifest. Use the setDisableAaidCollection() and isAaidCollectionDisabled() functions to disable AAID collection programmatically:

android/app/src/main/AndroidManifest.xml

xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- Remove AD_ID permission that comes from the SDK -->
    <uses-permission
        android:name="com.google.android.gms.permission.AD_ID"
        tools:node="remove" />

    <!-- Your other permissions -->
</manifest>

Make sure to add xmlns:tools="http://schemas.android.com/tools" to your manifest tag to use the tools:node="remove" attribute. If you disable AAID collection, you should also remove the AD_ID permission from your manifest to fully comply with Google Play's Family Policy requirements.

For more information, see Google Play Services documentation.

Backup Configuration ​

For Android apps, the SDK provides backup rules to exclude Shared Preferences data from backup. This prevents the retention of the Linkrunner install ID during reinstallation, ensuring accurate detection of new installs and re-installs.

For detailed backup configuration instructions, please refer to the Android SDK Backup Configuration.

Encrypted SharedPreferences ​

SDK Version Requirement: Starting from linkrunner v3.9.1, the SDK automatically encrypts the credentials it stores in Android SharedPreferences (such as the install ID and other persisted SDK state). No additional configuration is required — upgrade to v3.9.1 or above to get this behavior by default.

On Android, values written by the SDK are encrypted at rest, with a hardware-protected key generated on the device and stored in the Android Keystore. The key never leaves the device and is not bundled with the SDK.

If you are upgrading from an earlier version, the SDK will transparently migrate any existing plaintext entries to the encrypted store on the next read after the upgrade — no code changes are needed on your side.

iOS Configuration ​

  1. Update your iOS deployment target to iOS 15.0 or higher in your ios/Podfile:
ruby
platform :ios, '15.0'
  1. Add the following to your Info.plist file for App Tracking Transparency:
xml
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads and improve your app experience.</string>
  1. To enable SKAdNetwork postback copies to be sent to Linkrunner, add the following keys to your Info.plist file:
xml
<key>NSAdvertisingAttributionReportEndpoint</key>
<string>https://linkrunner-skan.com</string>
<key>AttributionCopyEndpoint</key>
<string>https://linkrunner-skan.com</string>

For complete SKAdNetwork integration details, see the SKAdNetwork Integration Guide.

Google Integrated Conversion Measurement (Optional) ​

Prefer to let your AI coding agent do this? The Flutter skill already covers ICM — adding the pod and wiring setConsent:

bash
npx @linkrunner/skills add flutter

See Linkrunner Agent Skills.

Integrated Conversion Measurement (ICM) recovers Google App Campaign installs on iOS that Google cannot attribute because there is no click identifier and no IDFA to match on. Google's On-Device Measurement (ODM) SDK turns the click context into an encrypted signal that never leaves the device, and Linkrunner sends it with the install. See Google ICM for how it works.

Set this up if you run Google App Campaigns for your iOS app. Requires linkrunner 4.1.1 or later.

Google keeps ODM inactive for users in the European Economic Area, the United Kingdom, and Switzerland, so ICM recovers nothing for that traffic. Elsewhere, Google reports improved coverage for iOS 14+ users.

ICM also needs an iOS link ID configured in your Google Ads integration. Google has nowhere to send the conversion without one. See Prerequisites.

Add Google's On-Device Measurement SDK (iOS)

Already using the Firebase iOS SDK 11.14.0 or later? The FirebaseAnalytics pod brings this SDK in for you. Skip this step.

The plugin does not bundle this SDK, so apps that skip ICM carry none of its weight. Add it inside the Runner target in ios/Podfile:

ruby
target 'Runner' do
  # ...your existing config

  pod 'GoogleAdsOnDeviceConversion'
end

Then install the pods:

bash
cd ios && pod install

CocoaPods adds the -ObjC and -lc++ linker flags for you, so there are no Xcode Build Settings to change.

Report consent

Set the values with setConsent before you call init, and again whenever the user changes their choice:

dart
import 'package:linkrunner/linkrunner.dart';
import 'package:linkrunner/models/lr_consent.dart';

await LinkRunner().setConsent(
  LRConsent(
    isEEA: ConsentStatus.GRANTED,
    hasConsentForDataUsage: ConsentStatus.GRANTED,
    hasConsentForAdsPersonalization: ConsentStatus.DENIED,
  ),
);

await LinkRunner().init('YOUR_PROJECT_TOKEN');

Each signal takes ConsentStatus.GRANTED, ConsentStatus.DENIED, or ConsentStatus.UNKNOWN. Anything omitted or left UNKNOWN is dropped from the payload rather than reported as a denial, so Linkrunner never reports a choice your user did not make.

ParameterMeaning
isEEAEuropean regulations apply to this user (the EEA, the UK, or Switzerland)
hasConsentForDataUsageThe user agreed to their data being sent to Google for advertising
hasConsentForAdsPersonalizationThe user agreed to their data being used to personalize ads

Google treats these as required whenever their value is known. hasConsentForDataUsage decides whether Google may use the conversion at all, hasConsentForAdsPersonalization decides whether it may feed audiences and remarketing, and isEEA tells Google which rules apply. Set them from your app's real consent state rather than hardcoding them. For users outside the EEA, the UK, and Switzerland, report isEEA as denied and leave the other two unset. See Send Consent.

setConsent works on both iOS and Android. Android has no ODM SDK to add, but its installs reach Google through the App Conversion API, which reads the same signals.

That is the whole integration. The native SDK fetches the value at initialization and there is no other Dart API to call. Attribution comes back through getAttributionData as usual.

Consent is stored between launches. Call setConsent again whenever the user's consent state changes, otherwise the previous value keeps being sent after the user has withdrawn it.

Verifying your setup ​

Initialize with debug set to true and look for this line in the Xcode console:

Linkrunner: odm_available=true odm_fetch_result=success odm_fetch_latency_ms=124

odm_available=false with odm_fetch_result=unavailable means Google's SDK is not linked. Check that pod install picked up GoogleAdsOnDeviceConversion.

Initialization (Required) ​

You'll need your project token to get started!

Note: The initialization method doesn't return any value. To get attribution data and deeplink information, use the getAttributionData method.

dart
import 'package:linkrunner/linkrunner.dart';

Future<void> initLinkrunner() async {
  try {
    // Initialize with your project token
    await LinkRunner().init(
      'YOUR_PROJECT_TOKEN',
      'YOUR_SECRET_KEY', // Optional: Required for SDK signing
      'YOUR_KEY_ID', // Optional: Required for SDK signing
      false, // Optional: Set to true to disable IDFA collection for iOS devices (defaults to false)
      true // Optional: Enable debug mode for development (defaults to false)
    );
    print('LinkRunner initialized');
  } catch (e) {
    print('Error initializing LinkRunner: $e');
  }
}

// Call this in your app's initialization
@override
void initState() {
  WidgetsFlutterBinding.ensureInitialized(); // Make sure this is added!
  super.initState();
  initLinkrunner();
}

SDK Signing Parameters (Optional) ​

For enhanced security, the LinkRunner SDK requires the following signing parameters during initialization:

  • secretKey: A unique secret key used for request signing and authentication
  • keyId: A unique identifier for the key pair used in the signing process

You can find your project token, secret key, and key ID here.

Platform-Specific SDK Signing ​

For applications requiring different signing keys per platform:

dart
import 'dart:io' show Platform;
import 'package:linkrunner/linkrunner.dart';

Future<void> initLinkrunnerWithSigning() async {
  try {
    // Initialize with your project token and SDK signing parameters
    await LinkRunner().init(
      'YOUR_PROJECT_TOKEN',
      Platform.isIOS ? 'YOUR_IOS_SECRET_KEY' : 'YOUR_ANDROID_SECRET_KEY', // Platform-specific secret key
      Platform.isIOS ? 'YOUR_IOS_KEY_ID' : 'YOUR_ANDROID_KEY_ID', // Platform-specific key ID
      true, // Optional: Enable debug mode for development (defaults to false)
    );
    print('LinkRunner initialized with SDK signing');
  } catch (e) {
    print('Error initializing LinkRunner: $e');
  }
}

Setting the Customer User ID ​

Use setCustomerUserId to attach your own user identifier to the device right after init. Once set, the identifier is stored securely on-device and automatically included in every event you track, so you never have to pass it on each trackEvent call.

Call it as early as the user's ID is available. This guarantees every event carries a user_id from the very first event, and is especially useful for existing users who were already onboarded before this feature shipped.

Available from Flutter SDK v3.10.0.

Best practice: set the Customer User ID as early as possible. The user_id is only attached to events tracked after it's set, and is not applied retroactively. Use a stable, unique identifier from your own system (for example your internal user ID or a UUID) rather than an email address or other PII.

dart
try {
  await LinkRunner().setCustomerUserId("f47ac10b-58cc-4372-a567-0e02b2c3d479"); // Your unique customer user ID (e.g. a UUID)
  print('Customer user id set');
} catch (e) {
  print('Error setting customer user id: $e');
}

The identifier is stored securely on-device and persists across app restarts. Calling setCustomerUserId again with a different identifier updates the stored value; passing the same identifier is a no-op. signup() / setUserData() also update it.

User Identification (Required) ​

Call the signup method as soon as the user is identified — whether through signup or login. This is the moment Linkrunner ties the install (and any future events) to a user identifier.

It is strongly recommended to use the integrated platform's identify function to set a persistent user_id once it becomes available (typically after signup or login).

If the platform's identifier function is not called, you must provide a user identifier for Mixpanel, PostHog, and Amplitude integration.

  • mixpanelDistinctId for Mixpanel
  • amplitudeDeviceId for Amplitude
  • posthogDistinctId for PostHog

The model classes (LRUserData, LRCapturePayment, LRRemovePayment, LRConsent) are not re-exported by linkrunner.dart. Import each one directly, for example import 'package:linkrunner/models/lr_user_data.dart';.

dart
Future<void> onSignup() async {
  try {
    await LinkRunner().signup(
      userData: LRUserData(
        id: '123', // Required: User ID
        name: 'John Doe', // Optional
        phone: '9876543210', // Optional
        email: 'user@example.com', // Optional
        // These properties are used to track reinstalls
        userCreatedAt: '2024-01-01T00:00:00Z', // Optional
        isFirstTimeUser: true, // Optional
        mixpanelDistinctId: 'mixpanelDistinctId', // Optional - Mixpanel Distinct ID
        amplitudeDeviceId: 'amplitudeDeviceId', // Optional - Amplitude User ID
        posthogDistinctId: 'posthogDistinctId', // Optional - PostHog Distinct ID
      ),
      data: {}, // Optional: Any additional data
    );
    print('Signup successful');
  } catch (e) {
    print('Error during signup: $e');
  }
}

To enable remarketing and reattribution, you need to capture deep links and pass them to the Linkrunner SDK. This allows Linkrunner to detect returning users who open the app via a deep link.

dart
import 'package:flutter/material.dart';
import 'package:app_links/app_links.dart';
import 'package:linkrunner/linkrunner.dart';

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _appLinks = AppLinks();

  @override
  void initState() {
    super.initState();
    _initLinkRunner();
  }

  Future<void> _initLinkRunner() async {
    // Init SDK first
    await LinkRunner().init('your_project_token');

    // Cold start — app was launched by a deeplink
    final initialLink = await _appLinks.getInitialLink();
    if (initialLink != null) {
      LinkRunner().handleDeeplink(initialLink.toString());
    }

    // Warm start — app was in background, deeplink brought it to foreground
    _appLinks.uriLinkStream.listen((Uri uri) {
      LinkRunner().handleDeeplink(uri.toString());
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // your app content
    );
  }
}

Linkrunner sends the updated deeplink back after processing. For Linkrunner campaign links, use the returned deeplink as the resolved destination instead of the original tracking URL.

json
{
    "deeplink": "https://app.yourdomain.com/product/123"
}

Getting Attribution Data ​

Using Amplitude for install or pre-login onboarding reports? Send attribution from your app before tracking those events. The automatic signup integration does not add campaign properties to earlier events.

To get attribution data and deeplink information for the current installation, use the getAttributionData function:

dart
Future<void> getAttributionInfo() async {
  try {
    final attributionData = await LinkRunner().getAttributionData();
    print('Attribution data: $attributionData');
  } catch (e) {
    print('Error getting attribution data: $e');
  }
}

The getAttributionData function returns an AttributionData object with the following structure:

dart
class AttributionData {
  final String? deeplink;            // Optional: The deep link URL that led to app installation
  final CampaignData campaignData;   // Required: Campaign information
}

class CampaignData {
  final String id;                   // Required: Campaign ID
  final String name;                 // Required: Campaign name
  final String? adNetwork;           // Optional: "META" | "GOOGLE" | null
  final String? groupName;           // Optional: Campaign group name
  final String? assetGroupName;      // Optional: Asset group name
  final String? adNetworkCampaignId; // Optional: Ad network campaign ID
  final String? adSetId;             // Optional: Ad set ID
  final String? adSetName;           // Optional: Ad set name
  final String? adCreativeId;        // Optional: Ad creative ID
  final String? adCreativeName;      // Optional: Ad creative name
  final String? assetName;           // Optional: Asset name
  final String type;                 // Required: Campaign type ("ORGANIC" | "INORGANIC")
  final String installedAt;          // Required: Installation timestamp
  final String storeClickAt;         // Required: Store click timestamp
}

Example response:

dart
// getAttributionData() returns a typed AttributionData object — access fields in camelCase.
{
  "deeplink": "https://app.yourdomain.com/product/123",
  "campaignData": {
    "id": "camp_123",
    "name": "Summer Sale 2024",
    "adNetwork": "META",
    "groupName": "iOS Campaign",
    "assetGroupName": "Product Catalog",
    "assetName": "Banner Ad 1",
    "adNetworkCampaignId": "120214682829390250",
    "adSetId": "120214682829640250",
    "adSetName": "Productivity",
    "adCreativeId": "120214682926100250",
    "adCreativeName": "Static_2",
    "type": "INORGANIC",
    "installedAt": "2024-03-20T10:30:00Z",
    "storeClickAt": "2024-03-20T10:29:45Z"
  }
}

Setting User Data ​

Call setUserData each time the app opens and the user is logged in:

setUserData is optional and is not a replacement for signup. Always call signup first as soon as the user is identified (signup or login). Use setUserData afterwards only when additional user details become available later — for example, when the user adds a phone number, email, or completes their profile after identification.

dart
Future<void> setUserData() async {
  try {
    await LinkRunner().setUserData(
      userData: LRUserData(
        id: '123', // Required: User ID
        name: 'John Doe', // Optional
        phone: '9876543210', // Optional
        email: 'user@example.com', // Optional
        mixpanelDistinctId: 'mixpanelDistinctId', // Optional - Mixpanel Distinct ID
        amplitudeDeviceId: 'amplitudeDeviceId', // Optional - Amplitude User ID
        posthogDistinctId: 'posthogDistinctId', // Optional - PostHog Distinct ID
      ),
    );
    print('User data set successfully');
  } catch (e) {
    print('Error setting user data: $e');
  }
}

Setting CleverTap ID ​

Use the setAdditionalData method to set CleverTap ID:

dart
Future<void> setIntegrationData() async {
  try {
    await LinkRunner().setAdditionalData(
      integrationData: {
        'clevertap_id': 'YOUR_CLEVERTAP_USER_ID', // CleverTap user identifier
      },
    );
    print('CleverTap ID set successfully');
  } catch (e) {
    print('Error setting CleverTap ID: $e');
  }
}

Parameters for LinkRunner.setAdditionalData ​

  • clevertap_id: String (optional) - CleverTap user identifier

This method allows you to connect user identities across different analytics and marketing platforms.

Revenue Tracking ​

Revenue is stored for all users, including organic ones. Payments from users with no matching click are stored without campaign attribution. Call .signup so payments are linked to a user. You can verify your events are being captured on the Events page.

Capturing Payments ​

Track payment information:

dart
Future<void> capturePayment() async {
  try {
    await LinkRunner().capturePayment(
      capturePayment: LRCapturePayment(
        amount: 99.99, // Required: Payment amount
        userId: 'user123', // Required: User identifier
        paymentId: 'payment456', // Required: Unique payment identifier
        type: PaymentType.FIRST_PAYMENT, // Optional: Payment type
        status: PaymentStatus.PAYMENT_COMPLETED, // Optional: Payment status
        eventData: { // Optional: Ecommerce/custom event data
          'content_ids': ['product_123'],
          'content_type': 'product',
          'currency': 'USD',
          'value': 99.99,
          'num_items': 1,
          'order_id': 'order_12345',
          'contents': [
            {
              'id': 'product_123',
              'quantity': 1,
              'item_price': 99.99
            }
          ]
        },
      ),
    );
    print('Payment captured successfully');
  } catch (e) {
    print('Error capturing payment: $e');
  }
}

Parameters for LRCapturePayment ​

  • amount: double (required) - The payment amount
  • userId: String (required) - Identifier for the user making the payment
  • paymentId: String (required) - Unique identifier for the payment, used to deduplicate transactions
  • type: PaymentType (optional) - Type of payment. Available options:
    • PaymentType.FIRST_PAYMENT - First payment made by the user
    • PaymentType.WALLET_TOPUP - Adding funds to a wallet
    • PaymentType.FUNDS_WITHDRAWAL - Withdrawing funds
    • PaymentType.SUBSCRIPTION_CREATED - New subscription created
    • PaymentType.SUBSCRIPTION_RENEWED - Subscription renewal
    • PaymentType.ONE_TIME - One-time payment
    • PaymentType.RECURRING - Recurring payment
    • PaymentType.DEFAULT_PAYMENT - Default type (used if not specified)
  • status: PaymentStatus (optional) - Status of the payment. Available options:
    • PaymentStatus.PAYMENT_INITIATED - Payment has been initiated
    • PaymentStatus.PAYMENT_COMPLETED - Payment completed successfully (default if not specified)
    • PaymentStatus.PAYMENT_FAILED - Payment attempt failed
    • PaymentStatus.PAYMENT_CANCELLED - Payment was cancelled
  • eventData: Map<String, dynamic> (optional) - Key-value pairs for additional event data, including ecommerce properties for Meta and Google.

Removing Payments ​

Remove payment records (for refunds or cancellations):

dart
Future<void> removePayment() async {
  try {
    await LinkRunner().removePayment(
      removePayment: LRRemovePayment(
        userId: 'user123', // Either userId or paymentId must be provided
        paymentId: 'payment456', // Optional: Unique payment identifier
      ),
    );
    print('Payment removed successfully');
  } catch (e) {
    print('Error removing payment: $e');
  }
}

Parameters for LRRemovePayment ​

  • userId: String (required) - Identifier for the user whose payment is being removed
  • paymentId: String (optional) - Unique identifier for the payment to be removed

Note: Either paymentId or userId must be provided when calling removePayment. If only userId is provided, all payments for that user will be removed.

Ecommerce Events ​

Minimum SDK Version: Ecommerce Event Manager requires linkrunner v3.7.0 or above. Please ensure your SDK is updated before using this feature.

If you are tracking Ecommerce events to sync with Meta or Google, you must format your eventData to include the required fields. You also need to map your custom event to the standard commerce event in the Linkrunner Dashboard.

For detailed explanations of the required fields like content_ids, contents, and value, refer to our Meta Commerce Manager documentation or Google Commerce Manager documentation.

Add To Cart Example ​

Use the trackEvent method to send an AddToCart event:

dart
Future<void> trackAddToCart() async {
  try {
    await LinkRunner().trackEvent(
      eventName: 'add_to_cart', // Map this custom event to "AddToCart" (Meta) or "add_to_cart" (Google) in the Linkrunner Dashboard. See: /ecommerce-manager/meta-commerce-manager, /ecommerce-manager/google-commerce-manager
      eventData: {
        'content_ids': ['product_123'],
        'contents': [
          {
            'id': 'product_123', // Matches content_ids
            'quantity': 1,
            'item_price': 49.99
          }
        ],
        'content_type': 'product',
        'currency': 'USD',
        'value': 49.99,
        'num_items': 1
      },
    );
    print('Add To Cart event tracked successfully');
  } catch (e) {
    print('Error tracking Add To Cart event: $e');
  }
}

View Content Example ​

Use the trackEvent method to send a ViewContent event:

dart
Future<void> trackViewContent() async {
  try {
    await LinkRunner().trackEvent(
      eventName: 'view_item', // Map this custom event to "ViewContent" (Meta) or "view_item" (Google) in the Linkrunner Dashboard. See: /ecommerce-manager/meta-commerce-manager, /ecommerce-manager/google-commerce-manager
      eventData: {
        'content_ids': ['product_123'],
        'contents': [
          {
            'id': 'product_123', // Matches content_ids
            'quantity': 1,
            'item_price': 49.99
          }
        ],
        'content_type': 'product',
        'currency': 'USD',
        'value': 49.99,
        'num_items': 1
      },
    );
    print('View Content event tracked successfully');
  } catch (e) {
    print('Error tracking View Content event: $e');
  }
}

Payment / Purchase Example ​

Use the capturePayment method to send a Purchase event containing the ecommerce payload:

dart
Future<void> capturePurchase() async {
  try {
    await LinkRunner().capturePayment(
      capturePayment: LRCapturePayment(
        amount: 49.99,
        userId: 'user123',
        paymentId: 'payment_456',
        type: PaymentType.FIRST_PAYMENT, // Map this payment type to "Purchase" (Meta) or "ecommerce_purchase" (Google) in the Linkrunner Dashboard. See: /ecommerce-manager/meta-commerce-manager, /ecommerce-manager/google-commerce-manager
        status: PaymentStatus.PAYMENT_COMPLETED,
        eventData: {
          'content_ids': ['product_123'],
          'contents': [
            {
              'id': 'product_123', // Matches content_ids
              'quantity': 1,
              'item_price': 49.99
            }
          ],
          'content_type': 'product',
          'currency': 'USD',
          'value': 49.99,
          'num_items': 1,
          'order_id': 'order_abc123' // Required for Purchase events
        },
      ),
    );
    print('Purchase captured successfully');
  } catch (e) {
    print('Error capturing purchase: $e');
  }
}

Note: For more information on testing and verifying your ecommerce events, please see our Meta Commerce Manager or Google Commerce Manager guide.

Tracking Custom Events ​

From Flutter SDK v3.10.0, custom events automatically include the user_id you set during signup() / setUserData(). The SDK stores this identifier securely on-device and attaches it to every trackEvent call, so you no longer need to pass it manually. Events tracked before signup are sent without a user_id.

Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Call .signup so events are linked to a user. You can verify your events are being captured on the Events page. For capturing revenue, it is recommended to use the .capturePayment method instead of .trackEvent.

Track custom events in your app:

dart
Future<void> trackEvent() async {
  try {
    await LinkRunner().trackEvent(
      eventName: 'purchase_initiated', // Event name
      eventData: { // Optional: Event data
        'product_id': '12345',
        'category': 'electronics',
        'amount': 99.99, // Include amount as a number for revenue sharing with ad networks like Google and Meta
      },
      eventId: 'order_12345', // Optional: Your own unique event identifier (String or num)
    );
    print('Event tracked successfully');
  } catch (e) {
    print('Error tracking event: $e');
  }
}

Parameters for LinkRunner().trackEvent ​

  • eventName: String (required) - Name of the event to track
  • eventData: Map<String, dynamic> (optional) - Key-value pairs for additional event data, including Meta ecommerce properties
  • eventId: Object (optional) - Your own unique identifier for the event (String or num), useful for deduplication and correlating with your backend

Revenue Sharing with Ad Networks ​

To enable revenue sharing with ad networks like Google Ads and Meta, include an amount parameter as a number in your custom event data. This allows the ad networks to optimize campaigns based on the revenue value of conversions:

dart
Future<void> trackPurchaseEvent() async {
  try {
    await LinkRunner().trackEvent(
      eventName: 'purchase_completed',
      eventData: {
        'product_id': '12345',
        'category': 'electronics',
        'amount': 149.99, // Revenue amount as a number
      },
    );
    print('Purchase event with revenue tracked successfully');
  } catch (e) {
    print('Error tracking purchase event: $e');
  }
}

For revenue sharing with ad networks to work properly, ensure the amount parameter is passed as a number (double or int), not as a string.

Enhanced Privacy Controls ​

The SDK offers options to enhance user privacy:

dart
// Enable PII (Personally Identifiable Information) hashing
LinkRunner().enablePIIHashing(true);

When PII hashing is enabled, sensitive user data like name, email, and phone number are hashed using SHA-256 before being sent to Linkrunner servers.

Disabling AAID Collection ​

SDK Version Requirement: The AAID collection disable functionality requires Flutter SDK version 3.5.0 or higher.

The SDK provides options to disable AAID (Google Advertising ID) collection. This is useful for apps targeting children or families to comply with Google Play's Family Policy.

Disabling AAID collection is not recommended unless absolutely necessary. The GAID is a primary signal for Google Ads attribution and install matching, so disabling it reduces attribution accuracy. Only disable it if your app is built for children or families and must comply with Google Play's Family Policy.

Disable AAID Collection ​

To disable AAID collection, call setDisableAaidCollection before SDK initialization:

dart
// Disable AAID collection
LinkRunner().setDisableAaidCollection(true);

// Check if AAID collection is disabled
bool isDisabled = LinkRunner().isAaidCollectionDisabled();

When AAID collection is disabled, the SDK will not collect or send the Google Advertising ID (GAID) to Linkrunner servers.

Removing AD_ID Permission ​

If you want to completely remove the AD_ID permission from your app's manifest (for example, for apps targeting children), you can override the SDK's permission declaration in your android/app/src/main/AndroidManifest.xml. For detailed instructions on revoking the AD_ID permission, including Google's policy requirements for apps targeting children and Android 13+ (API 33+), see the Revoking the AD_ID Permission section above.

Uninstall Tracking ​

Before you begin ​

Here's what you need to know before getting started:

Requirements:

Android ​

Connect Firebase Cloud Messaging (FCM) with Linkrunner

FCM HTTP v1 API

To configure FCM HTTP V1 for uninstalls:

Enable the FCM API:

  1. Go to the FCM console.
  2. Select a project.
  3. Go to Project Overview > Project settings.
  4. Copy the Project ID. This will be required in a later step. Project ID
  5. Go to the Cloud Messaging tab.
  6. Make sure that Firebase Cloud Messaging API (V1) is set to Enabled.

Create a custom role for Linkrunner Uninstall:

  1. Go to the Service accounts tab.
  2. Click Manage service account permissions.
  3. A new browser tab opens in Google Cloud Platform.
  4. In the side menu, select Roles.
  5. Click + Create role.
  6. Enter the following details:
    • Title: Linkrunner Uninstalls
    • ID: lr_uninstalls
    • Role launch stage: General availability
  7. Click + Add permissions.
  8. In Enter property name or value field, enter cloudmessaging.messages.create and select it from the search results. Google Cloud Permission
  9. Check the cloudmessaging.messages.create option and click Add.
  10. Click Create.

Assign Linkrunner the FCM uninstall role:

  1. In the side menu, select IAM.
  2. Open the View by Principals tab.
  3. Click Grant Access.
  4. In Add Principals -> New principals field, enter lr-uninstalls-tracking@lr-uninstalls-tracking.iam.gserviceaccount.com
  5. In Assign Roles -> Select a role field, enter Linkrunner Uninstalls and select it from the search results.
  6. Click Save.

The Linkrunner service account has been assigned the role of Linkrunner Uninstalls.

Linkrunner Dashboard
  1. In Linkrunner, go to Settings > Uninstall Tracking.
  2. Under the Android tab, enter the Firebase Project ID that you copied initially and click Save.

Uninstall Tracking

Integrate with Linkrunner SDK

Follow these instructions to integrate FCM with the Linkrunner SDK:

  1. Set up Firebase Cloud Messaging:

Set up Firebase Cloud Messaging in your flutter app. See the Firebase Cloud Messaging documentation for detailed instructions.

  1. Configure your app to provide the device's push token to the Linkrunner SDK.
dart
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:linkrunner/linkrunner.dart';

class MyFirebaseMessagingService {

    static Future<void> initialize() async {
        // Fetch FCM token and set in Linkrunner SDK
        String? token = await FirebaseMessaging.instance.getToken();
        if (token != null) {
            await LinkRunner().setPushToken(token);
        }
    }

    static void setupTokenRefresh() {
        // Receive new FCM token and set in Linkrunner SDK
        FirebaseMessaging.instance.onTokenRefresh
            .listen((fcmToken) async {
                await LinkRunner().setPushToken(fcmToken);
            })
            .onError((err) {
                // Error getting token.
            });
    }

    static void setupMessageListener() {
        FirebaseMessaging.onMessage.listen((RemoteMessage message) {
            if (message.data.containsKey("lr-uninstall-tracking")) {
                return;
            } else {
                // Handle other data payloads here
            }
        });
    }
}

Custom implementations of FCM's onMessageReceived method can unintentionally make uninstall push notifications visible to users, disrupting the intended silent experience. To avoid this, ensure your logic checks if the message contains lr-uninstall-tracking and handles it accordingly, as shown in the code example above.

iOS ​

Connect APNs with Linkrunner

Apple Developer Portal

Get the required credentials from the Apple Developer Portal:

APNs Authentication Key (p8) and Key ID:

  • Go to the Apple Developer Portal.
  • Select Identifiers under Certificates, IDs & Profiles.
  • Click on the app you want to track uninstalls for. Then, under Capabilities, search for Push Notifications and enable it.
  • Under Certificates, IDs & Profiles, select Keys and click on plus (+) icon to create a key. Enable APNs when creating the key and download the key file (p8).
  • The Key ID can be found in the Keys tab.

Bundle ID and Team ID:

  • Under Identifiers, click on your app and you will see the Bundle ID and Team ID (App ID Prefix).
Linkrunner Dashboard
  1. In Linkrunner, go to Settings > Uninstall Tracking.
  2. Under the iOS tab, upload the APNs Authentication Key (p8) file and enter the Key ID, Bundle ID and Team ID (App ID Prefix) that you copied from the Apple Developer Portal.

Uninstall Tracking

Integrate with Linkrunner SDK

Follow these instructions to integrate FCM with the Linkrunner SDK:

  1. Set up Firebase Cloud Messaging:

Set up Firebase Cloud Messaging in your flutter app if you haven't already. See the Firebase Cloud Messaging documentation for detailed instructions.

  1. Configure your app to provide the device's APNs token to the Linkrunner SDK.
dart
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:linkrunner/linkrunner.dart';

class MyFirebaseMessagingService {

    static Future<void> initialize() async {
        // Fetch APNs token and set in Linkrunner SDK
        String? token = await FirebaseMessaging.instance.getAPNSToken();
        if (token != null) {
            await LinkRunner().setPushToken(token);
        }
    }
}

Function Placement Guide ​

FunctionRequirementWhere to PlaceWhen to Call
LinkRunner().initRequiredApp initializationOnce when app starts
LinkRunner().signupRequiredIdentification flow (signup or login)Once when the user is identified
LinkRunner().setCustomerUserIdRequiredApp initialization or identification flowAfter init, as soon as your user ID is available
LinkRunner().handleDeeplinkRequiredDeep link entry pointsWhen app is opened via a deep link
LinkRunner().getAttributionDataOptionalAttribution data handling flowWhenever the attribution data is needed
LinkRunner().setAdditionalDataOptionalIntegration codeWhen third-party integration IDs are available
LinkRunner().setUserDataOptionalAuthentication or profile update logicAfter signup, when additional user details become available
LinkRunner().trackEventOptionalThroughout appWhen specific user actions occur, including ecommerce events
LinkRunner().capturePaymentOptionalPayment processingWhen user makes a payment, including ecommerce purchases
LinkRunner().removePaymentOptionalRefund flowWhen payment needs to be removed
LinkRunner().setPushTokenOptionalPush notification setupWhen FCM (Android) or APNs (iOS) token is available
LinkRunner().setConsentOptionalApp initialization or consent flowBefore init, and again when consent changes
LinkRunner().enablePIIHashingOptionalPrivacy settingsWhen you want to enable PII hashing
LinkRunner().setDisableAaidCollectionOptionalApp initialization or privacy settingsBefore init, when you need to disable AAID collection on Android
LinkRunner().isAaidCollectionDisabledOptionalPrivacy settings or compliance checksWhen you need to check AAID collection status on Android

Complete Example ​

Here's a simplified example showing how to integrate Linkrunner in a Flutter app:

You can find your project token here.

dart
import 'package:flutter/material.dart';
import 'package:linkrunner/linkrunner.dart';

final linkrunner = LinkRunner();

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Linkrunner Demo',
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  bool _initialized = false;

  @override
  void initState() {
    super.initState();
    _initializeLinkrunner();
  }

  Future<void> _initializeLinkrunner() async {
    try {
      await LinkRunner().init('YOUR_PROJECT_TOKEN');
      setState(() {
        _initialized = true;
      });
    } catch (e) {
      print('Error initializing LinkRunner: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Linkrunner Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('LinkRunner ${_initialized ? 'Initialized' : 'Initializing...'}'),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () async {
                await LinkRunner().trackEvent(eventName: 'button_clicked');
              },
              child: Text('Track Custom Event'),
            ),
          ],
        ),
      ),
    );
  }
}

Next Steps ​

Support ​

If you encounter issues during integration, contact us at support@linkrunner.io.