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
- 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.
- 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. - Choose the host. Recommended is your first-party proxy with
@x3growth/sdk1.3.0 or later, for examplehttps://nordlicht.app/n3: the same domain then carries your X3 links and Apple's postback copies. Without a proxy the SDKs usehttps://api.x3growth.io. - 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) oralways. The choice is yours and so is the legal responsibility (consent in apps). The samples below showopt_infirst, 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:
pod 'X3Kit', '~> 1.0'
pod 'X3Kit/UI', '~> 1.0'Configure once at launch, with opt_in and the consent sheet on the first launch:
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.
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:
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-nativeAdd the config plugin and rebuild the native projects (npx expo prebuild or an EAS build):
{
"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:
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 x3growthConfigure in main, with opt_in and the consent sheet:
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
- Start a debug build on a simulator, emulator or device. Debug builds are marked as test traffic
automatically (
test: trueforces it,test: falseturns it off), so they do not change your reports. - With
opt_in, accept in the consent sheet (nothing is sent before). - 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).
- 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.