Documentation
Android (Kotlin)
Add the SDK, initialise it, sign beacons, bind purchases, forward deep links. Twenty minutes.
Requirements
- Android 5.0+ (
minSdk 21). The dependency resolves from Maven Central, which every Android project already has. - The SDK declares two small Play Services artifacts itself (advertising id and App Set ID) and merges the
AD_IDpermission and the Install Referrer receiver into your manifest. No manual manifest work. - Play Console → App content → Data safety must declare that the app collects the Advertising ID. That declaration is per app, not per release track, so it touches your live listing; get it agreed before the first upload that contains the SDK.
1. Add the dependency
// app/build.gradle.kts
dependencies {
implementation("com.roassensor:roas:0.1.6")
}2. Initialise
Once, as early as possible: Application.onCreate() is the right place. Initialising in a deep screen risks missing the earliest events of the one visit that matters most, the open right after an ad click.
import com.roassensor.sdk.Roas
class App : Application() {
override fun onCreate() {
super.onCreate()
Roas.initialize(
this,
publicKey = "YOUR_PUBLIC_KEY",
baseUrl = "https://api.roassensor.com",
appSecret = BuildConfig.ROAS_APP_SECRET.ifEmpty { null },
)
}
}baseUrl is printed by the panel and must be passed explicitly. The SDK's built-in default is the production host; an app pointed at the wrong environment compiles, runs, and posts every beacon into a void with nothing to say so.
initialize is idempotent: a second call is ignored, so calling it from more than one entry point is harmless.
3. The app secret
It comes from a build-time variable, never a source file. Put it in local.properties (gitignored) or your CI's secret store, and expose it as a BuildConfig field:
// app/build.gradle.kts
val roasAppSecret: String = (project.findProperty("ROAS_APP_SECRET") as String?)
?: System.getenv("ROAS_APP_SECRET")
?: ""
android {
buildFeatures { buildConfig = true }
defaultConfig {
buildConfigField("String", "ROAS_APP_SECRET", "\"$roasAppSecret\"")
}
}local.properties: ROAS_APP_SECRET=…. Gradle reads it as a project property.The .ifEmpty { null } in the initialise call is not cosmetic: an empty string signs with an empty key and every beacon is rejected as invalid. Absent must mean null.
4. Bind purchases to the install
Play's purchase notification names the purchase token and the event, not the buyer. The one field that carries our visitor id through a purchase is obfuscatedAccountId on the billing flow. Set it, or every sale books as unattributed.
val vid = Roas.visitorId() // "rs" + 32 hex, or null before initialize()
val flowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.apply { vid?.let { setObfuscatedAccountId(it) } }
.build()
billingClient.launchBillingFlow(activity, flowParams)Verify it once on an internal build: the Purchase object Play returns carries accountIdentifiers.obfuscatedAccountId. If it is null, the field was not set.
Optional, 0.1.8+: report the purchase the moment Billing settles it, instead of waiting for Play's notification. The SDK only ever names the purchase; the amount is read from Google. It dedupes against the notification that follows, so nothing double-books.
// In your PurchasesUpdatedListener, once purchaseState == PURCHASED
Roas.verifyPurchase(purchase.purchaseToken, productId, isSubscription = true)Revenue itself comes from Play's notifications; see Google Play revenue.
5. Deep links
A Play install carries its click through the Install Referrer automatically. A tap on an ad by someone who already has the app arrives as an App Link instead, so forward it:
// In the activity that receives your intent filter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.dataString?.let { Roas.handleDeepLink(it) }
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
intent.dataString?.let { Roas.handleDeepLink(it) }
}The whole query string is forwarded, so rsclid, gclid, fbclid and any utm_* or rs_* parameters on the link all attribute the open. If the app handles no links, the call is harmless -- it never fires.
6. Identity and funnel events
Play gives us no buyer email, so without this the visitor id is the single thread between a sale and its campaign. Call identify whenever an email becomes known; it is idempotent.
Roas.identify(email = user.email) // hashed on device
Roas.track(RoasEvent.BEGIN_CHECKOUT, properties = mapOf(
RoasProps.PRODUCT_ID to "pro_yearly", // must equal the Play product id
))RoasEvent covers the usual funnel: VIEW_CONTENT, ADD_TO_CART, BEGIN_CHECKOUT, SIGN_UP, LOGIN, START_TRIAL, SUBSCRIBE, LEVEL_START, LEVEL_COMPLETE and more. RoasEvent.CUSTOM with a name covers anything else. Hashed email also raises match rates for the conversions we push back to Meta and Google Ads.
7. Debugging
Roas.setLogLevel(RoasLogLevel.DEBUG) // logcat tag: RoasSensor
Roas.setOnDeliveryResult { path, ok, error ->
Log.d("roas", "$path -> ${if (ok) "ok" else error}")
}A healthy first launch logs /api/tracking/mobile/first-open -> HTTP 201. The Install Referrer line beside it says where the install came from (play=OK, OK_ORGANIC, or an OEM store on Vivo, Huawei, Xiaomi and Samsung devices, which the SDK also reads).
Check it worked
- Install the build on a device and open it once.
- On the app's Overview tab, an install row appears within seconds with
sdk_versionmatching your dependency andsigned = true. - A build installed with
adbor from Android Studio shows an organic referrer. That is expected: only a Play-mediated install carries a referrer. Testing a mobile integration covers the real thing.

