App SDK reference

The iOS (X3Kit), Android (io.x3growth.sdk), React Native (@x3growth/react-native) and Flutter (x3growth) SDKs share one API with the same names. Common rules for every call:

  • safe to call from any thread, never blocks the caller for IO, never throws (exceptions: lead reports errors, and the React Native configure throws a TypeError without a consent mode);
  • processed in call order on one serial queue;
  • before configure a no-op with a debug log;
  • nothing is sent before the purpose analytics is granted (lead excepted).

Version of all four app SDKs: 1.0.0.

configure

PlatformSignature
SwiftX3.configure(key: String, options: X3Options)
KotlinX3.configure(context: Context, key: String, options: X3Options)
TypeScriptX3.configure(key: string, options: X3Options): void
Dartstatic Future<void> X3.configure(String key, X3Options options)

Validates the key (pk_3x_ plus 30 characters; otherwise an error log and the SDK stays off), loads the persisted state, registers lifecycle observers. Network activity starts only when analytics is granted: then it fetches the app config, starts or continues the session, schedules the install request and the automatic events. A second call with the same key is ignored; with another key it logs an error and is ignored. Kotlin uses context.applicationContext; call it in Application.onCreate.

Options

OptionTypeDefault
consent.modealways · opt_in · opt_outrequired, no defaultconsent modes
consent.purposes.analyticsBooltruefalse means the SDK sends nothing
consent.purposes.attributionBooltrueinstall attribution and conversion values
consent.purposes.recordingsBoolfalsesession recordings
consent.purposes.adPlatformsBoolfalseconversions to ad platforms
consent.bufferUntilDecisionBoolfalseopt_in only: keep up to 200 events of this process in memory until a decision
hostURLhttps://api.x3growth.ioAPI root or your proxy, e.g. https://nordlicht.app/n3
recording.modeoff · wireframeoffrecordings
recording.maskAllTextBooltrue
recording.maskAllImagesBooltrue
recording.screenshotScreens[String][]screen names or paths
recording.indicatorpill · dot · hiddenpill
recording.sampleRateDouble 0…11.0per session
recording.wifiOnlyBoolfalse
recording.sourcenative · externalnativefor wrappers only
autoScreensBooltrueUIKit view controllers, Android activities (wrappers always pass false)
autoAppEventsBooltrueapp_install, app_open, app_update
trackDeepLinksBooltrueevent deep_link_open and link attribution in handleOpenURL
testBool?automaticnil / null: debug builds are test traffic; true marks everything as test
debugBoolfalseverbose logs: os.Logger subsystem io.x3growth.sdk, Logcat tag X3
flushIntervalSecondsInt 2…12010the server config may lower or raise it
wrapperX3Wrapper?nilfor wrappers only: name and version

Spelling per platform: Swift X3ConsentOptions(mode: .optIn, purposes: .init(recordings: true)), enum cases .always / .optIn / .optOut, .off / .wireframe, .pill / .dot / .hidden; Kotlin X3ConsentMode.ALWAYS | OPT_IN | OPT_OUT, X3RecordingMode.OFF | WIREFRAME, X3Indicator.PILL | DOT | HIDDEN, data classes with named arguments; TypeScript string unions "always" | "opt_in" | "opt_out"; Dart enums X3ConsentMode.always | optIn | optOut.

Debug detection: Android FLAG_DEBUGGABLE; iOS simulator or a provisioning profile with get-task-allow. The effective behaviour is the code options and the project's server config; the stricter wins (recordings need recording.mode = wireframe, the purpose used and granted, and recordings enabled in the project).

screen

PlatformSignature
SwiftX3.screen(_ name: String, properties: X3Properties? = nil), SwiftUI .x3Screen(_ name: String)
KotlinX3.screen(name: String, properties: Map<String, Any>? = null)
TypeScriptX3.screen(name: string, properties?: X3Properties): void
DartX3.screen(String name, [Map<String, Object>? properties])

Ends the current screen view and starts a new one. With properties, the SDK also sends the event screen_view with them. Screens matching the project's excluded screen paths are not tracked.

Screen paths. Names become paths:

  1. trimmed; empty → /;
  2. starting with / (route paths): cut at ? or #, repeated / collapsed, trailing / dropped, at most 200 characters (/(tabs)/home?ref=push → /(tabs)/home);
  3. otherwise a name: the suffix ViewController, Controller, Activity, Fragment, Screen, Page or View is removed, camel case becomes kebab case, accents are removed, everything is lowercased and other characters become - (PaywallViewController → /paywall, SignUpViewController → /sign-up, Übersicht → /ubersicht, 🔥 Deals → /deals).

On the server the app's bundle id is the host of these pageviews, so screens appear in page reports as app.nordlicht/paywall.

track

PlatformSignature
SwiftX3.track(_ name: String, properties: X3Properties? = nil)
KotlinX3.track(name: String, properties: Map<String, Any>? = null)
TypeScriptX3.track(name: string, properties?: X3Properties): void
DartX3.track(String name, [Map<String, Object>? properties])

name matches ^[a-z][a-z0-9_]{0,39}$ (otherwise dropped with a debug log; no case conversion). Up to 20 properties; keys ^[A-Za-z][A-Za-z0-9_]{0,39}$; values string (up to 200 characters), finite number or boolean. The event attaches to the current screen view; before the first screen of a session it waits up to 2 seconds for one, then attaches to a screen view /. lead and purchase are dropped here: use the dedicated calls. Event names blocked in the project are dropped on the device.

identify

PlatformSignature
SwiftX3.identify(userId: String? = nil, email: String? = nil, leadId: String? = nil)
KotlinX3.identify(userId: String?, email: String? = null, leadId: String? = null)
TypeScriptX3.identify(userId: string, traits?: { email?: string; leadId?: string }): void
DartX3.identify(String userId, {String? email, String? leadId})

Hashes the user id, e-mail (plus the Gmail variant) and lead id with SHA-256 on the device, stores the hashes and sends them with the next batch and with the first batch of every new session until reset. Computes the account token. Never creates a lead. Users, logins and leads.

reset

X3.reset() on every platform. Forgets the identity and the account token, ends the current session (its data is sent) and starts a new one. The install id stays.

lead

PlatformSignature
Swifttry await X3.lead(_ lead: X3Lead), X3.lead(_ lead: X3Lead, completion: (Result<Void, X3Error>) -> Void)
Kotlinsuspend fun X3.lead(lead: X3Lead), X3.lead(lead: X3Lead, callback: X3Callback)
TypeScriptX3.lead(lead: X3Lead): Promise<void> (rejects with an Error whose code is an error code)
DartFuture<void> X3.lead(X3Lead lead) (throws X3Exception(code))

Posts the inquiry once over TLS, immediately; retries up to 3 times (2, 8, 30 seconds) on network errors and server errors, never stores it on disk. Works in every consent state, because the user sends an inquiry on purpose; the install and session ids are attached only while analytics is granted. The project's lead switch per app (Leads aus der App, default on), spam checks, CRM push, team notifications and the webhook lead.created apply as for web leads.

purchase

PlatformSignature
SwiftX3.purchase(_ purchase: X3Purchase), X3.purchase(_ transaction: StoreKit.Transaction)
KotlinX3.purchase(purchase: X3Purchase)
TypeScriptX3.purchase(purchase: X3Purchase): void
DartX3.purchase(X3Purchase purchase)

Sends the event purchase (order_id, value rounded to 2 decimals, currency uppercased, items, product, store) at once. The conversion is held until a server confirms the same order id. The StoreKit 2 overload takes Transaction.id, productID, purchasedQuantity and, on iOS 16 and later, price and currency. Purchases.

purchaseAccountToken

PlatformSignature
SwiftX3.purchaseAccountToken() -> UUID?
KotlinX3.purchaseAccountToken(): String?
TypeScriptX3.purchaseAccountToken(): Promise<string | null>
DartFuture<String?> X3.purchaseAccountToken()

A UUID version 5 of the bundle id and the hashed user id (namespace ac8463a7-8c0f-4776-908d-ce30d53b369d, name <bundle id>\n<user id hash>), available after identify. Pass it as StoreKit 2 appAccountToken and Play Billing setObfuscatedAccountId.

PlatformSignature
SwiftX3.consent(_ update: X3ConsentUpdate)
KotlinX3.consent(update: X3ConsentUpdate)
TypeScriptX3.consent(update: X3ConsentUpdate): void
DartX3.consent(X3ConsentUpdate update)

Records explicit decisions per purpose (analytics, attribution, recordings, adPlatforms; omitted purposes keep their state). Persisted before the next call runs. Transitions.

consentAll

X3.consentAll(granted) on every platform (Swift X3.consentAll(_ granted: Bool)): grants or denies every used purpose. The buttons Alle akzeptieren and Ablehnen of the consent sheet call it.

consentState

PlatformSignature
SwiftX3.consentState() -> X3ConsentState
KotlinX3.consentState(): X3ConsentState
TypeScriptX3.consentState(): Promise<X3ConsentState>
DartFuture<X3ConsentState> X3.consentState()

Returns the mode, the state of each purpose (granted, denied, pending, notUsed), optedOut and needsDecision (mode opt_in, a used purpose still pending, not opted out).

onConsentChange

PlatformSignature
SwiftX3.onConsentChange(_ handler: @escaping (X3ConsentState) -> Void) -> X3Subscription (main queue; cancel())
KotlinX3.addConsentListener(listener: X3ConsentListener): X3Subscription (a fun interface, so X3.addConsentListener { state -> } works; main thread)
TypeScriptX3.onConsentChange(listener: (state: X3ConsentState) => void): () => void
DartStream<X3ConsentState> X3.consentChanges

Called after every change of the effective consent state, including opt-out.

setOptOut and isOptedOut

PlatformSignature
SwiftX3.setOptOut(_ optOut: Bool), X3.isOptedOut() -> Bool
KotlinX3.setOptOut(optOut: Boolean), X3.isOptedOut(): Boolean
TypeScriptX3.setOptOut(optOut: boolean): void, X3.isOptedOut(): Promise<boolean>
DartFuture<void> X3.setOptOut(bool optOut), Future<bool> X3.isOptedOut()

true denies every used purpose with all effects (local data deleted); false returns to the mode's states and keeps explicit decisions. The switch Messung erlauben in the settings helper is bound to it.

flush

X3.flush() on every platform: sends queued data now, respecting a server's back-off after 429. The SDK also sends every flushIntervalSeconds, when enough events are queued, when the app goes to the background and after purchase.

handleOpenURL

PlatformSignature
SwiftX3.handleOpenURL(_ url: URL) -> Bool
KotlinX3.handleOpenURL(uri: Uri): Boolean, X3.handleIntent(intent: Intent): Boolean
TypeScriptX3.handleOpenURL(url: string): Promise<boolean> (called automatically for Linking)
DartFuture<bool> X3.handleOpenURL(Uri url) (called automatically by the plugin)

Returns true when the URL carried X3 attribution: an x3c parameter, any of the 13 attribution parameters, or a path <pathPrefix>/<code> on one of your link hosts (default prefix /n3/go). Records it as link attribution and sends deep_link_open { x3 } (with trackDeepLinks). Never stores the full URL. handleIntent reads intent.data and marks the intent as handled.

applyLinkToken

X3.applyLinkToken(token) on every platform: the same as a URL with x3c=<token>, for magic links and apps that obtain a link token themselves (link tokens).

requestTrackingAuthorization

PlatformSignature
Swiftawait X3.requestTrackingAuthorization() -> X3TrackingAuthorization
Kotlin–
TypeScriptX3.requestTrackingAuthorization(): Promise<X3TrackingAuthorization> (Android: "unavailable")
DartFuture<X3TrackingAuthorization> X3.requestTrackingAuthorization()

Shows Apple's ATT prompt (needs NSUserTrackingUsageDescription; without it returns the current status and logs) and stores the status, which the next batch reports. Results: authorized, denied, notDetermined, restricted, unavailable.

ids

PlatformSignature
SwiftX3.installId: String?, X3.sessionId: String?
KotlinX3.installId, X3.sessionId
TypeScriptX3.ids(): Promise<{ installId: string | null; sessionId: string | null }>
DartFuture<({String? installId, String? sessionId})> X3.ids()

For support requests, server calls (sessionId for leads.create or events from your server) and deletion by install id. null while analytics is not granted.

version

Swift X3.version, Kotlin X3.VERSION, TypeScript X3.version, Dart X3.version: "1.0.0".

eraseDeviceData

PlatformSignature
Swiftawait X3.eraseDeviceData() -> Bool (true when X3 confirmed the deletion)
Kotlinsuspend fun X3.eraseDeviceData() (throws X3Error), X3.eraseDeviceData(callback: X3Callback)
TypeScript, Dartnot in the 1.0.0 wrappers; delete by install id from your server (deleting a user)

Sends the install id and the device secret once to X3, which deletes this install's sessions, recordings and journey keys after checking the secret. Then the SDK deletes its local files (queue, install id, device secret, identity, config cache) except the consent choice, and continues with a new install id. Call setOptOut(true) as well when the user also wants no further measurement. If X3 cannot be reached or refuses, the local data is kept so you can try again. It needs an existing install: after an analytics denial the SDK has already deleted the install id and there is nothing left to erase on the device. Details: deleting the data of this device.

Types

TypeFields
X3Propertiesup to 20 entries of string, number or boolean
X3Leadform?, email?, phone?, firstName?, lastName?, company?, message?, answers? (questionnaire → question → option), marketingConsent?, crm? (CRM aliases), skipCrm (default false), fillMs?, eventId?
X3PurchaseorderId (required), value, currency, productId?, quantity (default 1), store (apple, google, other; default by platform)
X3ConsentUpdateanalytics?, attribution?, recordings?, adPlatforms?
X3PurposeStategranted, denied, pending, notUsed
X3ConsentStatemode, analytics, attribution, recordings, adPlatforms, optedOut, needsDecision
X3TrackingAuthorizationauthorized, denied, notDetermined, restricted, unavailable
X3Subscriptioncancel()
X3ConsentTextsevery text of the consent helpers, all optional, plus appName and the privacy policy URL (default texts)

Automatic events

Sent by the SDK when autoAppEvents (or the respective call) is on:

EventWhenProperties
app_installfirst session of an install (once per install id)none
app_openstart of every sessioncold (process start or return from the background)
app_updatefirst session after the app version changedfrom (previous version)
deep_link_openhandleOpenURL with trackDeepLinksx3 (the URL carried X3 attribution)
purchaseX3.purchaseorder_id, value, currency, items, product, store
screen_viewscreen(name, properties) with propertiesyour properties

Conventions (your app sends them; dashboard features read them): account_created (once at signup), login (every login, property method), trial_started (also created from store notifications).

Sessions: a session continues across app restarts while the last activity is less than 30 minutes ago; after 30 minutes in the background or closed, the next start begins a new session with app_open. Returning to a screen after less than 30 seconds in the background continues the same screen view. Data is queued on disk (at most 1 000 items or 7 days) while analytics is granted.

Framework helpers

HelperiOSAndroidReact NativeFlutter
screen namesX3ScreenNameProviding, SwiftUI .x3Screen("Paywall")X3ScreenNameProvider, Compose X3TrackNavigation(navController)useX3NavigationTracking(ref), useX3ExpoRouterTracking()X3NavigatorObserver()
maskingview.x3Masking = .mask / .unmask / .inherit, .x3Mask(), .x3Unmask()X3.setMasking(view, X3Masking.MASK / UNMASK / INHERIT), Modifier.x3Mask(), Modifier.x3Unmask()<X3Mask>, <X3Unmask>not in 1.0.0 (the Flutter view is recorded as one masked area)
tap target namesview.x3Anchor, .x3Anchor(…), accessibilityIdentifierX3.setAnchor(view, …), X3.setAnchorResolver { view -> String? }, Modifier.x3Anchor(…), testTagtestIDnot in 1.0.0
consent sheetX3ConsentView(texts:onDone:), X3.presentConsent(from:texts:completion:)X3ConsentDialog.show(activity, texts) { }, Compose X3ConsentSheet(...)<X3ConsentSheet visible onDone texts />, useX3Consent()X3ConsentSheet.show(context, texts: …)
settingsX3PrivacySettingsView(texts:), X3PrivacySettingsViewControllerX3PrivacySettingsDialog.show(activity, texts), Compose X3PrivacySettings(...)<X3PrivacySettings texts />X3PrivacySettings(texts: …)

For wrappers

Public on the native SDKs for wrapper authors (React Native and Flutter use them): the option wrapper (name react-native or flutter and version), the consent listener, iOS UIView.x3Masking / UIView.x3Anchor, Android X3.setMasking, X3.setAnchor, X3.setAnchorResolver.

External recording source: iOS X3.recordExternal(screenPath:events:fullSnapshot:) and Android X3.recordExternal(screenPath, events, fullSnapshot) with recording.source = external accept rrweb events produced by the wrapper for the current screen view; the native recorder packs and uploads them under the same consent, sampling and budget rules. No wrapper of version 1.0.0 uses it yet (Flutter records its view as one masked area).

Error codes

lead reports these errors (Swift X3Error, Kotlin X3Error, TypeScript error.code, Dart X3Exception.code):

CodeMeaning
notConfiguredconfigure was not called or the key is invalid
invalidthe server rejected the input (field limits, e-mail format)
disabledleads from this app are switched off (Leads aus der App)
rateLimitedmore than 3 inquiries per minute from this device
networkno connection after the retries
server(status)the server answered with another error status

Batches handle server answers themselves: 400 drops the batch, 403 (key, app_mismatch, disabled) stops sending until the next successful config fetch, 413 splits the batch, 429 waits for retryAfter, 503 retries with back-off (5 s, 30 s, 2 min, 10 min, then every 30 min). The wire format and every endpoint: app ingest API.