Crossdeck Docs
Dashboard

Android SDK quickstart

Quickstart 10 min · Kotlin · Android 5.0+ (API 21) · AGP 8.x · Java 17

From a fresh Crossdeck account to your first Android event landing in the dashboard in under ten minutes. One Gradle dependency, two values to paste, one Crossdeck.start(...) call inside your Application.

You can also do this from the onboarding wizard.

If you just signed up, the dashboard's Onboarding flow generates the same snippet you'll build here. This page exists for developers who closed the wizard, came back later, or want to read the steps before they paste anything into Android Studio.

Before you start

You'll need:

You don't need a Google Play Console account or a published listing to send your first event — the emulator or a USB-connected device works fine. Google Play rail connection comes later, in rail-webhooks.

Step 1 · Find your appId and publishable key

Crossdeck identifies your Android app with two values:

ValueWhat it looks likeWhat it does
appId app_android_92b2d6a5728a4d Identifies which app inside your project the events belong to. Stable, never rotates.
publicKey cd_pub_test_… or cd_pub_live_… Authenticates the SDK to the ingest endpoint. Safe to ship inside a Play Store APK/AAB.
Test keys for development, live keys for Play Store builds.

A cd_pub_test_… key routes into the sandbox environment — events here never affect production analytics or your customer-facing dashboards. Pair test keys with Environment.SANDBOX in debug builds and live keys with Environment.PRODUCTION in release — the SDK validates the pairing at start.

Where to find them

  1. Open the Crossdeck dashboard and select your project from the workspace switcher (top-left).
  2. From the left sidebar, click Developers, then API keys. The direct path is /dashboard/developers/api-keys/.
  3. The page lists your apps. If you haven't added an Android app yet, click + Add app and pick Android. Each app card shows the appId at the top and two publishable keys below — one test, one live. Click the copy icon next to either to copy it to your clipboard.

Step 2 · Find your Android applicationId

When you add an Android app in the Crossdeck dashboard, you'll be asked for its applicationId — the globally-unique reverse-DNS name that identifies your app on Google Play (com.acme.notes, com.spotify.music). Crossdeck uses it to attribute Google Play purchases and developer notifications to the right app inside your project.

How to find it in Android Studio

  1. Open your project in Android Studio.
  2. In the Project view (left sidebar, switch the dropdown to Android if it isn't already), open Gradle Scripts → build.gradle.kts (Module: app). If you're on Groovy DSL, it's build.gradle (Module: app).
  3. Inside the android { defaultConfig { } } block, find the applicationId line — usually shaped like applicationId = "com.yourcompany.yourapp".
applicationId is the Google Play identity, not the package name.

You'll see two similar-looking strings in an Android project: applicationId (in build.gradle) and package (in AndroidManifest.xml or the namespace in build.gradle). They often match, but they're allowed to diverge — and Google Play, Play Billing, and Play Console all key on applicationId. Use applicationId for Crossdeck.

If you have product flavors or build variants (debug, release, paid, free)

Flavors and the debug suffix can produce different applicationIds at install time — e.g. com.acme.notes.debug for debug builds, com.acme.notes for release. Use the release applicationId (no suffix) for the Crossdeck app entry. If you want telemetry from debug and release routed to separate Crossdeck apps, create two Crossdeck app entries and pick the right key in your build script with BuildConfig.DEBUG.

If you can't find Android Studio (or want the answer from the command line)

grep applicationId app/build.gradle.kts
# or, on Groovy DSL:
grep applicationId app/build.gradle

Step 3 · Add the Gradle dependency

The SDK ships from Maven Central as com.crossdeck:crossdeck:1.+ — a single AAR with one import.

Kotlin DSL (build.gradle.kts)

// settings.gradle.kts — confirm mavenCentral is in the resolution list
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}

// app/build.gradle.kts
dependencies {
    implementation("com.crossdeck:crossdeck:1.+")
}

Groovy DSL (build.gradle)

// app/build.gradle
dependencies {
    implementation 'com.crossdeck:crossdeck:1.+'
}

Sync Gradle (Android Studio offers a "Sync Now" banner the moment you save). After sync, import com.crossdeck.Crossdeck resolves and brings the entire public API into scope.

Step 4 · Create the Application class

The SDK should start exactly once per process, before any UI code runs. The standard Android idiom is a subclass of android.app.Application overriding onCreate.

If you already have an Application subclass, skip to Step 5 — you just need to add the Crossdeck.start(...) call. If you don't, create one now:

  1. In Project → app → java → com.yourcompany.yourapp (or wherever your package root is), right-click → New → Kotlin Class/File.
  2. Name it MyApplication and pick Class.
  3. Make it extend Application:
    package com.yourcompany.yourapp
    
    import android.app.Application
    
    class MyApplication : Application() {
        override fun onCreate() {
            super.onCreate()
            // Crossdeck.start goes here — Step 5.
        }
    }
  4. Wire it into your AndroidManifest.xml by adding the android:name attribute to the <application> tag:
    <!-- app/src/main/AndroidManifest.xml -->
    <application
        android:name=".MyApplication"
        android:label="@string/app_name"
        …>
        …
    </application>

Step 5 · Initialise the SDK

Call Crossdeck.start(context, options) exactly once inside Application.onCreate. Wrap the call in try/catch and store the client as nullable — a typo'd key should log + degrade telemetry, never crash a customer's launch.

package com.yourcompany.yourapp

import android.app.Application
import android.util.Log
import com.crossdeck.Crossdeck
import com.crossdeck.CrossdeckError
import com.crossdeck.CrossdeckOptions
import com.crossdeck.Environment

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // Build-configuration switch — Debug never embeds a live key,
        // Release never routes events into the sandbox.
        val publicKey = if (BuildConfig.DEBUG) "cd_pub_test_…" else "cd_pub_live_…"
        val env = if (BuildConfig.DEBUG) Environment.SANDBOX else Environment.PRODUCTION

        crossdeck = try {
            Crossdeck.start(
                this,
                CrossdeckOptions(
                    appId = "app_android_92b2d6a5728a4d",
                    publicKey = publicKey,
                    environment = env,
                ),
            )
        } catch (e: CrossdeckError) {
            // invalid_secret_key / env_mismatch / missing_app_id —
            // recoverable misconfig. Log + carry on without telemetry.
            Log.e("Crossdeck", "start failed (${e.code}): ${e.message}", e)
            null
        } catch (e: Throwable) {
            Log.e("Crossdeck", "start failed", e)
            null
        }
    }

    companion object {
        // Process-singleton accessor. Reach from anywhere as
        // \`MyApplication.crossdeck?.…\` — no
        // \`(applicationContext as MyApplication).cd\` boilerplate.
        var crossdeck: Crossdeck? = null
            private set
    }
}
Why nullable instead of lateinit?

If Crossdeck.start throws (typo'd key, env mismatch), lateinit var cd: Crossdeck stays uninitialised. The FIRST access from anywhere in your app then crashes with UninitializedPropertyAccessException — long after onCreate, in code that has nothing to do with the original misconfig. Nullable + safe-call (cd?.) means the host app keeps working with telemetry silently disabled.

Reach the client from anywhere as MyApplication.crossdeck?.…:

// Track an event
MyApplication.crossdeck?.track("paywall_seen", mapOf("variant" to "annual"))

// Synchronous paywall gate (safe from the UI thread)
if (MyApplication.crossdeck?.isEntitled("pro") == true) {
    showProUI()
} else {
    showPaywall()
}
The environment enum must match the publishable-key prefix.

cd_pub_test_… pairs with Environment.SANDBOX; cd_pub_live_… pairs with Environment.PRODUCTION. Mismatched values throw CrossdeckError.envMismatch at start rather than silently routing events into the wrong warehouse. Use BuildConfig.DEBUG to swap both together (the snippet above does this).

AGP 8.x trap: BuildConfig is off by default.

If your build fails with Unresolved reference: BuildConfig, your Android Gradle Plugin 8.x project hasn't enabled the BuildConfig class. Add it explicitly to app/build.gradle.kts:

android {
    buildFeatures {
        buildConfig = true
    }
}

Sync Gradle and BuildConfig.DEBUG resolves. (AGP 7 generated it automatically — 8 made it opt-in to shave a few hundred ms off cold builds.)

Step 6 · Identify your user

Until you call identify(...), every event carries an anonymous ID minted on first launch and persisted via SharedPreferences. The moment you know who the user is — after sign-in — call identify with your app's user ID. The SDK rotates the anonymous events onto the identified person automatically.

MyApplication.crossdeck?.identify("user_847")

// When the user signs out
MyApplication.crossdeck?.reset()

The userId is whatever your app uses — the database primary key, the Firebase Auth UID, the Google account ID. Stable for the lifetime of the account.

Want the People feed to show real names instead of anon_xxx? identify also takes optional email and traits — see Identify users for the full signature.

Step 7 · Verify the heartbeat

Build and run your app on an emulator or USB-connected device. The SDK fires its boot heartbeat the moment start returns. Open Developers → Heartbeat in the dashboard to confirm.

The page shows a per-app SDK signal audit:

End-to-end latency is 3–8 seconds.

The SDK batches events (default flush at 20 events or 5 seconds) → ingest writes to Firestore → the dashboard's onSnapshot listener fans the update out. If you don't see anything after 15 seconds, jump to Troubleshooting.

If nothing shows up after 30 seconds

SymptomMost likely causeFix
Logcat: Crossdeck start failed (env_mismatch) environment doesn't match the key (test key + PRODUCTION, or live key + SANDBOX). Use BuildConfig.DEBUG to swap both together — the snippet in Step 5 does this.
Logcat: Crossdeck start failed (invalid_app_id) The publishable key is for a different project than the appId. Re-copy both values from the same app card on API keys.
Build fails with Could not find com.crossdeck:crossdeck:1.+ Your settings.gradle doesn't include mavenCentral() in the dependency-resolution list. Add mavenCentral() to the repositories { } block inside dependencyResolutionManagement in settings.gradle.kts.
Build fails with Unresolved reference: BuildConfig AGP 8.x ships with BuildConfig generation disabled by default. Your Application class references BuildConfig.DEBUG. Enable it explicitly in app/build.gradle.kts: android { buildFeatures { buildConfig = true } }. Sync Gradle.
App launches but nothing fires from Application.onCreate Your AndroidManifest.xml doesn't reference .MyApplication on the <application> tag — Android is using the default Application. Add android:name=".MyApplication" to the <application> element. Step 4 covers the exact line.
No logcat errors, but Heartbeat shows nothing Emulator or device has no network; or a firewall is blocking api.cross-deck.com. From the device's browser, open https://api.cross-deck.com — you should see a 404 page (the root has no route, but DNS resolves). If it doesn't load, network isn't reachable.

What's next

Heartbeat green? The SDK is live. From here: