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:
// 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:
// 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.
// :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).
// :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".
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:
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.
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.
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.
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.
// 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.
| Key | Type | Description |
|---|---|---|
$email | string | Primary email, often the merge key for identity stitching. |
$phone | string | E.164 phone number. |
$name | string | Full display name. |
$first_name | string | Given name. |
$last_name | string | Family name. |
$avatar_url | string | Full URL to the user's avatar image. |
Geo
Geographic context.
| Key | Type | Description |
|---|---|---|
$country | string | ISO 3166 country code. |
$city | string | City name. |
$region | string | State or province. |
$timezone | string | IANA zone like America/Los_Angeles. |
$language | string | IETF tag like en or ru-RU. |
$locale | string | Full locale identifier. |
Lifecycle
When did we see them.
| Key | Type | Description |
|---|---|---|
$created | ISO8601 | Signup or account creation time. |
$last_seen | ISO8601 | Last engagement time. |
Subscription
Set if your product has plans.
| Key | Type | Description |
|---|---|---|
$plan | string | Tier slug — free, pro, enterprise. |
$subscription_status | string | active / trial / cancelled / past_due. |
$trial_ends | ISO8601 | When the current trial expires. |
$mrr | number | Monthly recurring revenue in account currency. |
$subscription_started | ISO8601 | When the current subscription began. |
E-commerce
Set if you sell products.
| Key | Type | Description |
|---|---|---|
$lifetime_orders | number | Count of completed orders. |
$lifetime_revenue | number | Total spend. |
$aov | number | Average order value. |
$last_purchase | ISO8601 | Most recent successful purchase. |
$first_purchase | ISO8601 | First successful purchase. |
$cart_abandoned_count | number | Lifetime count of cart abandonments. |
Media
Set if you publish content.
| Key | Type | Description |
|---|---|---|
$content_tier | string | free / premium / paid. |
$subscribed_categories | CSV string or array | Categories the user follows. |
$watch_time_total | number | Lifetime watch time in seconds. |
$last_played | ISO8601 | Most recent playback start. |
Marketplace
Set if you're a two-sided platform.
| Key | Type | Description |
|---|---|---|
$seller_tier | string | Seller-side tier slug. |
$buyer_tier | string | Buyer-side tier slug. |
$listings_count | number | Active listings the user owns. |
$reviews_count | number | Reviews the user has received. |
$verified | boolean | KYC status. |
Loyalty
Set for engagement and rewards programs.
| Key | Type | Description |
|---|---|---|
$loyalty_points | number | Current redeemable points balance. |
$vip_level | string | VIP tier slug. |
$referral_count | number | Successful 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.
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.
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.
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:
import io.kixo.sdk.Kixo
Kixo.logPushOpened(payload = pushPayload) // open
Kixo.logPushOpened(payload = pushPayload, actionId = "reply") // action-button tap
Kixo.logPushDismissed(payload = pushPayload) // swipe-awaySession 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).
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.
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.
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:
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.
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:
// 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.