App tracking quickstart

Five steps for every stack: register the app, install the SDK, call configure with a consent mode, track a screen, verify. The examples use the demo app Nordlicht (bundle id and package name app.nordlicht).

Before you start

  1. In the dashboard open Einstellungen › Apps (Settings › Apps) and click App verbinden (Connect app). Choose the platform, enter the bundle id (iOS) or package name (Android) exactly as in your build, a name and, for iOS, the App Store id if the app is already live.
  2. Copy the app key (pk_3x_…). Every registered app has its own publishable key; it is bound to the platform and bundle id, so a website key does not work in the app and the other way round. For an app on iOS and Android register two apps in the same project.
  3. Choose the host. Recommended is your first-party proxy with @x3growth/sdk 1.3.0 or later, for example https://nordlicht.app/n3: the same domain then carries your X3 links and Apple's postback copies. Without a proxy the SDKs use https://api.x3growth.io.
  4. Choose the consent mode: opt_in (nothing is sent until the user agrees), opt_out (on by default, the user can switch it off in your settings) or always. The choice is yours and so is the legal responsibility (consent in apps). The samples below show opt_in first, with the ready-made consent sheet.

You decide how consent works in your app, and you are responsible for that decision. X3 implements your choice technically but does not check whether it is legally right for your users and countries. Clarify the legal basis with your privacy counsel and describe X3 in your privacy policy and in the App Store and Google Play privacy details.

The placeholder pk_3x_YOUR_APP_KEY stands for your app key in every sample.

iOS

Install. In Xcode choose File › Add Package Dependencies…, enter https://github.com/X3Growth/x3-sdk-ios and add the products X3Kit and X3KitUI (and X3KitReplay if you want recordings). With CocoaPods:

Podfile
pod 'X3Kit', '~> 1.0'
pod 'X3Kit/UI', '~> 1.0'

Configure once at launch, with opt_in and the consent sheet on the first launch:

NordlichtApp.swift
import SwiftUI
import X3Kit
import X3KitUI

@main
struct NordlichtApp: App {
  @State private var askConsent = false

  init() {
    X3.configure(key: "pk_3x_YOUR_APP_KEY", options: X3Options(
      consent: X3ConsentOptions(mode: .optIn),
      host: URL(string: "https://nordlicht.app/n3")!
    ))
  }

  var body: some Scene {
    WindowGroup {
      ContentView()
        .onAppear { askConsent = X3.consentState().needsDecision }
        .sheet(isPresented: $askConsent) {
          X3ConsentView(texts: X3ConsentTexts(privacyPolicyURL: URL(string: "https://nordlicht.app/privacy"))) { _ in
            askConsent = false
          }
        }
        .onOpenURL { X3.handleOpenURL($0) }
    }
  }
}

The other modes only change one line: X3ConsentOptions(mode: .optOut) (on by default, put X3PrivacySettingsView() into your settings screen) or X3ConsentOptions(mode: .always) (no dialog; the settings view shows the switch Messung erlauben / Allow measurement).

First screen. UIKit view controllers are tracked automatically. In SwiftUI mark screens:

PaywallView().x3Screen("Paywall")

Full guide: iOS SDK (Swift).

Android

Install.

app/build.gradle.kts
dependencies {
    implementation("io.x3growth:sdk:1.0.0")
    implementation("io.x3growth:sdk-compose:1.0.0") // optional: Compose helpers and consent UI
}

Configure in Application.onCreate, with opt_in:

app/src/main/java/app/nordlicht/NordlichtApp.kt
package app.nordlicht

import android.app.Application
import io.x3growth.sdk.X3
import io.x3growth.sdk.X3ConsentMode
import io.x3growth.sdk.X3ConsentOptions
import io.x3growth.sdk.X3Options

class NordlichtApp : Application() {
    override fun onCreate() {
        super.onCreate()
        X3.configure(this, "pk_3x_YOUR_APP_KEY", X3Options(
            consent = X3ConsentOptions(mode = X3ConsentMode.OPT_IN),
            host = "https://nordlicht.app/n3",
        ))
    }
}

Show the consent dialog on the first launch of your main activity:

if (X3.consentState().needsDecision) {
    X3ConsentDialog.show(this, X3ConsentTexts(privacyPolicyUrl = "https://nordlicht.app/privacy")) { state -> }
}

X3ConsentMode.OPT_OUT and X3ConsentMode.ALWAYS work the same way; put X3PrivacySettingsDialog.show(activity, texts) (or Compose X3PrivacySettings(...)) behind a row in your settings.

First screen. Activities are tracked automatically. With Compose Navigation, inside the composable that owns the NavController:

X3TrackNavigation(navController)

Full guide: Android SDK (Kotlin).

React Native and Expo

Install.

npx expo install @x3growth/react-native

Add the config plugin and rebuild the native projects (npx expo prebuild or an EAS build):

app.json
{
  "expo": {
    "plugins": [["@x3growth/react-native", { "linkDomains": ["nordlicht.app"], "attributionEndpoint": "https://nordlicht.app" }]]
  }
}

Bare React Native apps run npx install-expo-modules@latest once, then npm install @x3growth/react-native and npx pod-install.

Configure at startup, with opt_in and the consent sheet:

app/_layout.tsx
import { useEffect, useState } from "react";
import { Stack } from "expo-router";
import { X3, X3ConsentSheet, useX3ExpoRouterTracking } from "@x3growth/react-native";

X3.configure("pk_3x_YOUR_APP_KEY", { consent: { mode: "opt_in" }, host: "https://nordlicht.app/n3" });

export default function RootLayout() {
  useX3ExpoRouterTracking();
  const [ask, setAsk] = useState(false);
  useEffect(() => {
    X3.consentState().then((state) => setAsk(state.needsDecision));
  }, []);
  return (
    <>
      <Stack />
      <X3ConsentSheet visible={ask} onDone={() => setAsk(false)} privacyPolicyUrl="https://nordlicht.app/privacy" />
    </>
  );
}

configure throws a TypeError when consent.mode is missing: there is no hidden default. For opt_out or always put <X3PrivacySettings /> on your settings screen.

First screen. useX3ExpoRouterTracking() (Expo Router) or useX3NavigationTracking(navigationRef) (React Navigation) reports every route change.

Full guide: React Native and Expo.

Flutter

Install.

flutter pub add x3growth

Configure in main, with opt_in and the consent sheet:

lib/main.dart
import 'package:flutter/material.dart';
import 'package:x3growth/x3growth.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await X3.configure('pk_3x_YOUR_APP_KEY', const X3Options(
    consent: X3ConsentOptions(mode: X3ConsentMode.optIn),
    host: 'https://nordlicht.app/n3',
  ));
  runApp(MaterialApp(navigatorObservers: [X3NavigatorObserver()], home: const HomePage()));
}

// on the first screen, after the first frame:
// if ((await X3.consentState()).needsDecision) await X3ConsentSheet.show(context, privacyPolicyUrl: 'https://nordlicht.app/privacy');

First screen. X3NavigatorObserver reports every named route (RouteSettings.name); call X3.screen('Paywall') for unnamed routes.

Full guide: Flutter.

Verify

  1. Start a debug build on a simulator, emulator or device. Debug builds are marked as test traffic automatically (test: true forces it, test: false turns it off), so they do not change your reports.
  2. With opt_in, accept in the consent sheet (nothing is sent before).
  3. In the wizard App verbinden, step Prüfen (Check), the page polls every 3 seconds and shows the SDK version, the consent mode, Debug-Build and the first screen as soon as the first batch arrives. Later the same status and the setup checklist are on Einstellungen › Apps › Nordlicht › Einrichtung (Settings › Apps › Setup); the checklist says what is still missing (store notifications, X3 links, postback copies, ad platforms).
  4. Leave the app or call flush() to send at once; otherwise batches go out every 10 seconds and when the app moves to the background.

Debug logs

Pass debug: true (Swift X3Options(consent: …, debug: true), Kotlin debug = true, React Native and Flutter debug: true). iOS logs with os.Logger under the subsystem io.x3growth.sdk (filter in Console.app or the Xcode console), Android with the Logcat tag X3. The logs say why something was not sent: consent not granted, invalid event name, screen excluded, key and bundle id do not match the registered app (app_mismatch), app disabled. More in troubleshooting.