Android SDK

The Kixo Android SDK supports Kotlin 2.0+ and Java, targeting minSdk 24 (Android 7.0) through targetSdk 35. A single Kixo.configure call in your Application.onCreate auto-tracks screens, taps, sessions, crashes, network requests, push notifications, and lifecycle events — and opens the door to session replay, identity, goals, and consent.

Quick start

Three files. Add the Maven repo, add the dependency, then drop two lines into your Application subclass.

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("https://raw.githubusercontent.com/kixoio/kixo-android-sdk/main/repo")
        }
    }
}

Note

That's the whole integration. The SDK turns every auto-tracker on by default. Override individual flags with KixoConfiguration.Builder(...) only when needed.

Add to your app

The Kixo Maven repo is hosted on GitHub Pages. Add it alongside google() and mavenCentral() in settings.gradle.kts:

kotlin
// settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("https://raw.githubusercontent.com/kixoio/kixo-android-sdk/main/repo")
        }
    }
}

Then declare the dependency in your app module:

kotlin
// app/build.gradle.kts
dependencies {
    implementation("io.kixo:kixo-android-sdk:0.1.8")
}

Tip

Internet permission is bundled in the SDK manifest — you don't need to add android.permission.INTERNET yourself. Network-state and notification permissions stay optional and are declared by your app when you opt into those features.

Multi-module projects

Gradle's implementation configuration is not transitive: declaring implementation("io.kixo:kixo-android-sdk:0.1.8") in a library module (e.g. :core_domain) does NOT make Kixo visible to :app or any other consumer. Two patterns work — pick one.

Pattern A — every module that calls Kixo declares it (recommended).Keeps each module's classpath minimal and avoids cascading rebuilds. Use a version catalog (libs.kixo.sdk) so you only edit the version in one place.

kotlin
// :core_domain/build.gradle.kts
dependencies {
    implementation("io.kixo:kixo-android-sdk:0.1.8")   // local use only
}

// :app/build.gradle.kts
dependencies {
    implementation(project(":core_domain"))
    implementation("io.kixo:kixo-android-sdk:0.1.8")   // declared again — fine
}

Pattern B — re-export through api(...).Single declaration, but the library module's public ABI now includes Kixo types — any version bump rebuilds every downstream module. Use this only when the library re-uses Kixo types in its own public signatures (e.g. returns KixoDiagnostics from a function).

kotlin
// :core_domain/build.gradle.kts
dependencies {
    api("io.kixo:kixo-android-sdk:0.1.8")              // re-exposed
}

// :app/build.gradle.kts
dependencies {
    implementation(project(":core_domain"))            // gets Kixo for free
}

Warning

If you see Unresolved reference: Kixo at compile time in a module, that module is missing its own dependency on the SDK — add the implementation line above, or use Pattern B.

Initialize

Configure Kixo from your Application subclass — onCreate runs before any activity, so every screen view, tap, and lifecycle event is captured from the first frame. Register the Application in your manifest with android:name=".MyApp".

kotlin
import android.app.Application
import io.kixo.sdk.Kixo

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Kixo.configure(
            context   = this,
            projectId = "kx_proj_YOUR_PROJECT_ID",
            apiKey    = "kx_key_YOUR_API_KEY",
        )
    }
}

For fine-grained tunables (auto-track flags, flush cadence, replay sampling, custom api host) build a KixoConfiguration explicitly:

kotlin
import io.kixo.sdk.Kixo
import io.kixo.sdk.KixoConfiguration

val config = KixoConfiguration.Builder(
    projectId = "kx_proj_YOUR_PROJECT_ID",
    apiKey    = "kx_key_YOUR_API_KEY",
)
    .autoTrackScreens(true)
    .autoTrackTaps(true)
    .autoTrackNetwork(true)
    .autoTrackCrashes(true)
    .autoTrackSessions(true)
    .autoTrackPush(true)
    .flushIntervalMillis(30_000)
    .flushAt(20)
    .maxBufferSize(200)
    .build(applicationContext)

Kixo.configure(this, config)

Note

Idempotent. A second configure call from the same process is a WARN-logged no-op — the SDK keeps the first configuration. Events queued by your auth singleton before configure lands are buffered (cap 50) and replayed once the SDK is wired up, so you can call Kixo.identify(...) from a global before Application.onCreate finishes.

Track events

Three primitives carry the bulk of your instrumentation: track for events, markGoal for conversion signals, and addBreadcrumb for non-event context.

kotlin
import io.kixo.sdk.Kixo

Kixo.track("video_played", mapOf(
    "video_id"    to "vid_42",
    "duration_ms" to 18_500,
    "autoplay"    to false,
))

Kixo.markGoal("activated", mapOf(
    "step"  to "onboarding_completed",
    "value" to 1,
))

Kixo.addBreadcrumb(
    message  = "user toggled dark mode",
    category = "ui",
    level    = "info",
)

Tip

Goals are graded.Marked goals feed Kixo's activation funnels and the daily change-detection cron — a goal whose volume drops 70% week-over-week shows up in your dashboard with a Needs review badge. Use markGoal for the handful of moments that matter; track for everything else.

Standard events

Sugar over Kixo.trackfor the events Kixo recognises by name — verbatim string keys that the backend's standard-event detector matches. Compile-time validation of property shape, single source of truth on naming.

kotlin
import io.kixo.sdk.Kixo
import io.kixo.sdk.SubscriptionInterval

Kixo.trackPurchase(
    amount    = 49.99,
    currency  = "USD",
    productId = "pro_yearly",
)

Kixo.trackSubscriptionStart(
    plan     = "pro",
    amount   = 9.99,
    currency = "USD",
    interval = SubscriptionInterval.MONTH,
)

Kixo.trackSignup(method = "google")
Kixo.trackTrialStart(plan = "pro", days = 14)
Kixo.trackCancel(plan = "pro", reason = "too_expensive")
Kixo.trackUpgrade(fromPlan = "free", toPlan = "pro")
Kixo.trackActivation(event = "first_post_published")
Kixo.trackShare(channel = "twitter", contentId = "post_123")
Kixo.trackInvite(channel = "email", recipientCount = 5)

Identify users

Bind subsequent events to a stable user id and a bag of traits. Anonymous-to-known stitching happens server-side — events captured before identify are retroactively attributed to the same user.

kotlin
import io.kixo.sdk.Kixo

// Reserved standard property keys carry a $-prefix (Mixpanel
// convention) so they namespace away from your own custom traits
// and promote to the dashboard's profile columns. See
// io.kixo.sdk.StandardProperty for the typed catalogue, or the
// "Standard property catalog" section below for the full 37-key list.
Kixo.identify("user_123", mapOf(
    "$email" to "jane@example.com",     // identity
    "$name"  to "Jane Doe",              // identity
    "$plan"  to "pro",                   // subscription pack
    "$lifetime_orders" to 12,            // e-commerce pack
    "signup_source" to "twitter_ad",     // custom trait
))

// Logout: clear identity, super-properties, and the persisted queue.
Kixo.reset()

Tag a user for segmentation

Use setUserProperty with a boolean value to attach a simple yes/no tag to the user. The tag persists across launches and powers segments, email campaigns, and chat queries — no setup beyond the SDK call.

kotlin
// Tag a user as subscribed — segments + campaigns can target this
Kixo.setUserProperty("subscribe", true)

// VIP membership
Kixo.setUserProperty("vip", true)

// String + numeric values work too
Kixo.setUserProperty("plan_tier", "enterprise")
Kixo.setUserProperty("lifetime_orders", 42)

// Bulk-set
Kixo.setUserProperties(mapOf(
    "subscribe" to true,
    "plan_tier" to "enterprise",
))

Properties persist via SharedPreferences across launches and auto-attach to every outbound event. In chat say things like "send a welcome email to users where subscribe is true" — Kixo builds the segment and drafts the template for you. Cleared on Kixo.reset().

Standard property catalog

Reserved property keys carry a $prefix so they namespace away from your custom traits. Kixo's catalog covers 37 keys across 3 universal packs (identity, geo, lifecycle) and 5 B2B vertical packs (subscription, e-commerce, media, marketplace, loyalty). Set whichever apply to your product — the dashboard adapts and renders only the packs you populate.

Identity

Always relevant. Sets the profile header columns.

KeyTypeDescription
$emailstringPrimary email, often the merge key for identity stitching.
$phonestringE.164 phone number.
$namestringFull display name.
$first_namestringGiven name.
$last_namestringFamily name.
$avatar_urlstringFull URL to the user's avatar image.

Geo

Geographic context.

KeyTypeDescription
$countrystringISO 3166 country code.
$citystringCity name.
$regionstringState or province.
$timezonestringIANA zone like America/Los_Angeles.
$languagestringIETF tag like en or ru-RU.
$localestringFull locale identifier.

Lifecycle

When did we see them.

KeyTypeDescription
$createdISO8601Signup or account creation time.
$last_seenISO8601Last engagement time.

Subscription

Set if your product has plans.

KeyTypeDescription
$planstringTier slug — free, pro, enterprise.
$subscription_statusstringactive / trial / cancelled / past_due.
$trial_endsISO8601When the current trial expires.
$mrrnumberMonthly recurring revenue in account currency.
$subscription_startedISO8601When the current subscription began.

E-commerce

Set if you sell products.

KeyTypeDescription
$lifetime_ordersnumberCount of completed orders.
$lifetime_revenuenumberTotal spend.
$aovnumberAverage order value.
$last_purchaseISO8601Most recent successful purchase.
$first_purchaseISO8601First successful purchase.
$cart_abandoned_countnumberLifetime count of cart abandonments.

Media

Set if you publish content.

KeyTypeDescription
$content_tierstringfree / premium / paid.
$subscribed_categoriesCSV string or arrayCategories the user follows.
$watch_time_totalnumberLifetime watch time in seconds.
$last_playedISO8601Most recent playback start.

Marketplace

Set if you're a two-sided platform.

KeyTypeDescription
$seller_tierstringSeller-side tier slug.
$buyer_tierstringBuyer-side tier slug.
$listings_countnumberActive listings the user owns.
$reviews_countnumberReviews the user has received.
$verifiedbooleanKYC status.

Loyalty

Set for engagement and rewards programs.

KeyTypeDescription
$loyalty_pointsnumberCurrent redeemable points balance.
$vip_levelstringVIP tier slug.
$referral_countnumberSuccessful referrals attributed to this user.

Tip

Don't see your pattern? Use bare keys for custom traits. They surface in the dashboard's Custom Traits panel without polluting the profile columns. The 5 vertical packs above are opinionated guesses at the most common B2B shapes — customer-specific terminology (e.g. shipping_plan) stays bare.

Super-properties

Per-session key/value pairs auto-attached to every outbound event. Different from identify traits (which describe identity); super-properties describe session context — active A/B variant, build flavour, opted-in feature flags. Persisted across launches; cleared on reset(). Per-event properties on track always win on collision.

kotlin
import io.kixo.sdk.Kixo

Kixo.setSuperProperty("build_flavour", "beta")
Kixo.setSuperProperties(mapOf(
    "ab_variant"        to "B",
    "referrer_campaign" to "autumn-launch",
))

// Sugar for A/B tracking — stored as 'experiment_<id>'.
Kixo.setExperimentVariant("checkout_v2", "variant_a")

Kixo.unsetSuperProperty("build_flavour")
Kixo.clearSuperProperties()

Push notifications

Two integration paths. Pick A if you're on FCM and want the shortest working setup; pick B if you already have a custom FirebaseMessagingServiceyou can't restructure, or you're on HMS / APNs.

Option A — extend KixoFirebaseMessagingService (auto-tracking)

Subclass KixoFirebaseMessagingService and call super.onMessageReceived(...) from your override — Kixo auto-emits push_received (visible payload) or push_silent (data-only). The base class also handles onNewToken registration if you don't override it. AndroidManifest.xml registration is unchanged from a normal FCM service.

kotlin
import com.google.firebase.messaging.RemoteMessage
import io.kixo.sdk.KixoFirebaseMessagingService

class MyMessagingService : KixoFirebaseMessagingService() {
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)  // Kixo auto-tracks push_received
        // … your own routing / notification display
    }
}

Note

Kixo does not add a Firebase dependency to your POM. The base class uses reflection to read RemoteMessage.getData() and getNotification()— Firebase stays a host-app dependency, exactly as if you weren't using Kixo at all.

Option B — call manual API from your own FCM service

Register your FCM token with Kixo via FirebaseMessagingService.onNewToken, then log each delivery explicitly. Use this path on HMS/APNs, or when you want Kixo to see only a subset of deliveries.

kotlin
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import io.kixo.sdk.Kixo
import io.kixo.sdk.PushProvider

class MyMessagingService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        Kixo.setPushToken(token, PushProvider.FCM)
    }

    override fun onMessageReceived(message: RemoteMessage) {
        // Convert the FCM payload to a Map<String, Any?> and log it —
        // Kixo correlates this with the open / dismiss it sees later.
        Kixo.logPushReceived(message.data.toMap(), appState = "background")
    }
}

Foreground delivery, notification opens, dismissals, and action-button taps are captured by the lifecycle bridge automatically once a token is registered. If you build a custom notification UI you can supply the open + dismiss signals explicitly:

kotlin
import io.kixo.sdk.Kixo

Kixo.logPushOpened(payload = pushPayload)            // open
Kixo.logPushOpened(payload = pushPayload, actionId = "reply")  // action-button tap
Kixo.logPushDismissed(payload = pushPayload)         // swipe-away

Session replay

Capture structural snapshots of your UI on every tap or screen transition. Off by default — opt in via KixoConfiguration with a sample rate; the SDK uniformly samples sessions at that rate and skips capture entirely on the others (zero CPU cost off-sample).

kotlin
import io.kixo.sdk.Kixo
import io.kixo.sdk.KixoConfiguration

val config = KixoConfiguration.Builder(
    projectId = "kx_proj_YOUR_PROJECT_ID",
    apiKey    = "kx_key_YOUR_API_KEY",
)
    .replayEnabled(true)
    .replaySampleRate(0.1)               // 10 % of sessions
    .replayCaptureOnCellular(false)      // Wi-Fi only by default
    .build(applicationContext)

Kixo.configure(this, config)

Tip

Replay is behavioural, not pixel-perfect — Kixo captures view hierarchy + interaction taxonomy (tap / long-press / swipe / pinch / scroll-end), not screenshots. PII in text views is filtered at the SDK before upload. Operators can scrub the replay player alongside the timeline of events in the dashboard.

Privacy and consent

Two independent switches cover GDPR-style consent and a hard opt-out. Both are programmatic — Kixo never renders consent UI on your end users' devices.

kotlin
import io.kixo.sdk.Kixo

// Consent: drop events at enqueue time until granted. Default is
// 'granted' — call revokeConsent() before any tracking if your local
// regulations require explicit opt-in.
Kixo.revokeConsent()
Kixo.grantConsent()

// Hard opt-out: persists across launches, clears identity, drops the
// queue. Use this for a user-facing 'don't track me' toggle.
Kixo.optOut()
Kixo.optIn()

val muted: Boolean = Kixo.isOptedOut()

Debugging

Kixo.diagnostics()returns a read-only snapshot of the SDK's health — useful in a hidden debug screen or smoke test. Answers "why aren't my events flowing?" without a debugger.

kotlin
import io.kixo.sdk.Kixo

val diag = Kixo.diagnostics()
Log.d("Kixo", "queued=${diag.queue.bufferedEventCount}")
Log.d("Kixo", "retryTier=${diag.queue.retryTier}")     // healthy | rapid | slow | cooldown
Log.d("Kixo", "paused=${diag.paused}")                  // server kill-switch state
Log.d("Kixo", "lifecycleState=${diag.lifecycleState}")  // running | recovering | …

Force a flush from your test harness — blocks up to timeoutMs on a network round-trip:

kotlin
import io.kixo.sdk.Kixo

// Async fire-and-forget — returns immediately.
Kixo.flush()

// Blocking flavour for instrumentation tests. Never call on the main thread.
val landed: Boolean = Kixo.flushBlocking(timeoutMs = 5_000L)
assertTrue(landed)

Compose Navigation

Auto-tracked screen views work for Activity / Fragment routes out of the box. For Jetpack Compose Navigation, fire Kixo.screen from a LaunchedEffect keyed on the route — the SDK then sees one event per destination, regardless of recomposition count.

kotlin
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import io.kixo.sdk.Kixo

@Composable
fun AppNavHost(nav: NavHostController) {
    NavHost(navController = nav, startDestination = "home") {
        composable("home") {
            LaunchedEffect("home") { Kixo.screen("HomeScreen") }
            HomeScreen()
        }
        composable("settings") {
            LaunchedEffect("settings") { Kixo.screen("SettingsScreen") }
            SettingsScreen()
        }
    }
}

AI coding agents

The SDK's public surface is small and shaped for code-completion — every method is on the Kixo singleton, every Kotlin sample in this guide starts with import io.kixo.sdk.Kixo, and our README ships an "AI agent quick reference" block that tools like Claude Code, Cursor, and Codex can paste directly into their context. If your agent gets stuck, the canonical starter is:

kotlin
// Tell your AI coding agent:
// "Integrate the Kixo Android SDK using io.kixo:kixo-android-sdk
//  from https://raw.githubusercontent.com/kixoio/kixo-android-sdk/main/repo.
//  Call Kixo.configure(this, projectId, apiKey) in Application.onCreate.
//  Then use Kixo.track / Kixo.identify / Kixo.markGoal as needed."

Note

Each section above has been written with that workflow in mind — imports are always explicit, types are always named, and the SDK singleton is never aliased. Hand this page to your agent and let it drive.