Android SDK
The Kixo Android SDK supports Kotlin 2.0+ and Java, requires minSdk 24 (Android 7.0), and is built against compileSdk 35. Your host app remains responsible for its own targetSdk. A single Kixo.configure call in your Application.onCreate auto-tracks screens, taps, sessions, crashes, and lifecycle events. Push tracking requires the FCM bridge described below. Network request auto-tracking is not part of the current Android release. The SDK also supports session replay, identity, and goals.
Quick start
Three files. Add the Maven repo, add the dependency, then drop two lines into your Application subclass.
Build requirements: compileSdk 35, minSdk 24, Kotlin 2.0+ or Java, and Java 17 bytecode.
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 analytics integration. Standard auto-trackers are enabled by default; push still needs the FCM bridge below. 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.17")
}Tip
android.permission.INTERNET and android.permission.ACCESS_NETWORK_STATE are bundled in the SDK manifest. Notification permissions stay owned by your app and are declared when you opt into push features.
Multi-module projects
Gradle's implementation configuration is not transitive: declaring implementation("io.kixo:kixo-android-sdk:0.1.17") 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.17") // local use only
}
// :app/build.gradle.kts
dependencies {
implementation(project(":core_domain"))
implementation("io.kixo:kixo-android-sdk:0.1.17") // 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.17") // 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(false) // reserved; no-op in the current Android release
.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,
))
// markGoal(name, value?, currency?, properties?) — pass extra context
// through the named 'properties' argument (a Map can't be the 2nd
// positional arg; that slot is the Double 'value').
Kixo.markGoal("activated", properties = mapOf(
"step" to "onboarding_completed",
))
// Revenue goals use the typed value + currency parameters:
Kixo.markGoal("purchase_completed", value = 49.99, currency = "USD")
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.track for the events Kixo recognizes 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 in Kixo — 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()⚠️ Kotlin dollar-sign trap. Standard identity keys are $-prefixed ($email, $name, $first_name) — and in a Kotlin literal you must escape the dollar as "\$email". Writing "$email" string-interpolates your email variable, so the value silently lands as a custom trait and never populates the Audience email / name columns. Simplest fix — use the typed overload (SDK 0.1.13+), which is impossible to get wrong: Kixo.setUserProperty(StandardProperty.EMAIL, email).
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 flavor, 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_flavor", "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_flavor")
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 FirebaseMessagingService you can't restructure or you want explicit control over which FCM deliveries Kixo sees.
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 compiles this optional class against Firebase Messaging but does not add Firebase to your app transitively. The SDK declares Firebase as compileOnly; an app choosing this option must already depend on firebase-messaging, as any FCM receiver does.
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 when you want Kixo to see only a subset of FCM deliveries. Android delivery currently supports FCM only.
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")
}
}Android does not expose a universal lifecycle hook for notification opens, dismissals, or action buttons. Forward those signals from the notification intents or receivers your app creates:
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
Replay a real, visual reconstruction of what the user saw. On each capture the SDK encodes a compressed frame of the screen (a JPEG image) together with a structural snapshot of the view hierarchy, and uploads both — so the dashboard player can render pixel-accurate playback alongside the interaction timeline. Configure replay for the project in Dashboard → Settings → Session replay. The SDK automatically reads and refreshes that project policy, including masking, capture modes, and cellular-upload permission.
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",
)
.build(applicationContext)
Kixo.configure(this, config)When cellular upload is disabled, queued replay waits for an allowed network.
Tip
Mask before upload. Kixo captures pixels, so masking runs before anything leaves the device. Password and e-mail fields are auto-detected and redacted; text in structural snapshots passes through a PII filter; and any view you mark with setKixoMask(true) is rasterized to an opaque rectangle in the frame before the JPEG is encoded — its pixels never leave the device. Jetpack Compose screens are masked wholesale by default (call setKixoMask(false) on the outermost ComposeView to include a screen you have audited). Operators scrub the replay player alongside the event timeline in the dashboard.
Data collection
The SDK captures the data enabled in your project and the events and properties your application sends.
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", "paused=${diag.paused}") // collection paused state
Log.d("Kixo", "lifecycleState=${diag.lifecycleState}") // SDK lifecycle stateForce 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 variant for instrumentation tests. Never call on the main thread.
val landed: Boolean = Kixo.flushBlocking(timeoutMs = 5_000L)
assertTrue(landed)Compose Navigation
Activity / Fragment routes produce immediate screen_viewevents and structured screen_visit records with dwell and flow metadata 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.