Docs menuAll pages, quickstarts and this page’s contents
- Introduction
- Quickstarts
- Core concepts
- Sensitivity and blocking
- API reference
- API explorer
- Guides
- Keys & dashboard
- Glossary
- Android
- iOSsoon
- React Nativesoon
- Fluttersoon
Android SDK quickstart
Add device integrity to an Android app in about fifteen minutes. The SDK looks for what a browser cannot see: emulator farms, rooted handsets, cloned apps and live instrumentation. It hands your server a device id, and your server scores it exactly as it scores a web signup.
The SDK reports evidence and never a verdict. Scoring stays on your server, with your secret key, so a patched app cannot change the answer.
Add the SDK
The SDK is served from Kaidn’s own Maven repository at https://maven.kaidn.io. Add it beside mavenCentral() in settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://maven.kaidn.io") }
}
}Then the dependency:
dependencies {
implementation("io.kaidn:kaidn-android:1.0.0")
// Optional, strongly recommended. Without it everything still works and
// the SDK simply reports no Google attestation.
implementation("com.google.android.play:integrity:1.4.0")
}| requirement | value |
|---|---|
| minSdk | 21 |
| compileSdk | 34 |
| Language | Kotlin or Java |
| Runtime dependencies | None. The AAR pulls in nothing, so it cannot clash with your JSON or HTTP library. |
The library adds two permissions to your manifest, INTERNET and ACCESS_NETWORK_STATE, plus the Google Services read permission. All three are normal permissions: nothing is prompted. It does not use QUERY_ALL_PACKAGES.
Create a tracker locked to your app
In the dashboard open Device trackers and create a tracker. The form asks for a name and at least one allowed domain (use your website’s; it is not used for app traffic), and has a field for Android packages. That field is the app’s lock: a beacon from a package that is not listed is refused with a 403, and a tracker with no packages refuses every Android beacon.
Each entry is one of two shapes:
| entry | accepts |
|---|---|
com.example.app | any build with that package name, whoever signed it |
com.example.app@<cert-sha256> | only a build signed by that certificate. 64 hex characters, no colons. |
Pin the app signing certificate from Play Console, not the upload key. With Play App Signing, Google re-signs your app, so the build on your users’ phones carries the app signing certificate. Take its SHA-256 from Play Console under Release, Setup, App signing (labels vary by Play Console version), in the section titled “App signing key certificate”. Pin the upload key instead and every user fails the lock. Play Console prints the fingerprint with colons; remove them, because an entry that does not match the expected shape is dropped rather than stored.
Copy the publishable key (pk_live_…) when the dashboard shows it, because it is shown once. It is safe to ship in your APK: it can only write beacons, never read data or score, and it is useless outside the packages you listed. The package list is fixed when the tracker is created. To change it, make a new tracker and ship its key.
Debug builds are signed with a different key. If you pinned the Play certificate, your own debug builds will be refused. Make a second tracker for development with a name-only entry and use its key in your debug build type. Your test devices will also show as sideloaded and debuggable, which is accurate.
Collect on the device
There is no separate initialisation. Kaidn.collect is the entry point: call it around signup, login or cash-out, with your publishable key. It collects, requests attestation and sends the beacon on a background thread, then calls you back.
Kaidn.collect(
applicationContext,
Kaidn.Config(publishableKey = "pk_live_…"),
) { report ->
// Runs on a background thread, after the beacon has been answered.
val kaidnDeviceId = report.deviceId // null if no usable Android ID existed
Log.d("app", "delivered=${report.delivered} emulator=${report.signals.isEmulator}")
// ...now send kaidnDeviceId to your backend with the signup request
}| option | default | meaning |
|---|---|---|
publishableKey | required | Your tracker’s pk_live_… key. |
baseUrl | https://api.kaidn.io | API base. Cleartext is refused except on loopback. |
cloudProjectNumber | null | Needed only if you distribute outside Google Play. |
requestIntegrity | true | Set false to skip attestation and its network round trip. |
The callback’s Report tells you whether it worked: delivered (the API accepted the beacon), httpStatus, error, and attested (a Google-signed verdict was obtained). Wait for the callback before you submit the signup, so the device is on file when your server scores it. The SDK never throws into your flow: every platform read is guarded, and a signal it could not measure is null, never a guessed false.
Score it on your server
Send report.deviceId to your own backend with the signup, and pass it to POST /v1/score as device_id. This is the same pattern as the web tag’s kaidn_device_id field: the beacon already carried the device signals, and the score picks them up through the id. Use your secret key, on your server only.
curl -X POST https://api.kaidn.io/v1/score \ -H "x-api-key: $KAIDN_API_KEY" \ -H "content-type: application/json" \ -d '{ "event": "signup", "user_id": "u_2001", "email": "priya@gmail.com", "ip": "24.60.140.22", "device_id": "<the deviceId the app sent you>" }'
Email and phone checks belong here, not in the app: an app cannot hold a secret key, and a publishable key can be pulled out of any APK. Send email, phone and phone_country in the same call. For handling the verdict, and the server half in your language, see the server quickstarts.
Turn on Play Integrity (optional)
Everything the SDK measures runs on a device its owner controls, so a competent hook can rewrite it. Play Integrity is the one signal that does not: Google produces and signs it off the device, and an emulator fails the device check outright. Turning it on also makes the package lock stronger: the package and certificate are then read from Google’s signed verdict instead of the app’s own report.
- Keep the
com.google.android.play:integritydependency from step 1. Your app must be published on Google Play. - In Play Console, open your app, then Protected with Play, then Play Integrity API and Manage. Under “Classic requests”, edit Response encryption and choose Manage and download my response encryption keys. Menu labels change between Play Console versions; Google’s setup guide has the current ones.
- In Device trackers, choose Set up Integrity on the tracker and paste the two keys: the decryption key (base64, 32 bytes) and the verification key (base64 DER, an EC public key). The shape of each key is checked when you save. Provide both, or neither.
Kaidn decrypts and verifies the token itself, at the beacon, with no call to Google on the scoring path. The keys are write-only: nothing ever reads them back, and the dashboard only reports whether they are set. Then four things are checked: that Google’s own verdict passes, that the nonce belongs to this device and request, that the token is fresh, and that the signed package and certificate are in your lock.
No token is not a failed token. Devices without Play Services, sideloaded builds, some regional ROMs and tenants who have not pasted keys all arrive unattested. Those beacons are accepted and scored on the other signals. Only a verdict Google actually signed and that says no can fail. A token that cannot be opened with your keys is recorded as unattested too, because pasting the wrong key is far likelier than forging a Google signature. Distributing outside Play? Set cloudProjectNumber.
What the SDK detects
Each detection becomes a reason on the score. The wording below is the SDK’s field; the last column is the name it takes in sensitivity and blocking.
| detects | SDK field | shows up as |
|---|---|---|
| An emulator: BlueStacks, LDPlayer, Nox, the Android Virtual Device. One unambiguous tell is enough, or two circumstantial ones. | isEmulator | Android emulator |
| Root: su binaries, Magisk, KernelSU, a test-keys build. | isRooted | Rooted Android device |
| Live instrumentation: Frida, Xposed, LSPosed. When this fires, treat the device's other self-reported signals as untrusted. | isHooked | Frida / Xposed instrumentation |
| A cloned app: Parallel Space, Dual Space, Island and similar containers running your app in a sandbox. A managed work profile is not counted as cloning. | isCloned | App-cloning container (Parallel Space...) |
| Not installed from Google Play or one of the listed vendor stores (Amazon, Huawei, Samsung). | isSideloaded | App not installed from Google Play |
| A debug build of your app. | isDebuggable | Debug build of the app |
| A known GPS-spoofing app is installed. This is the weaker claim: it does not prove the location is being faked, and the SDK never asks for the location permission. | isMockLocation | GPS-spoofing app installed |
| A VPN is switched on in the device. | isVpnActive | VPN switched on in the device |
| The device clock is ahead of the server’s. The phone sends its clock and your result is compared with the time the beacon arrived, so it is measured, not guessed. Ahead only, by more than 15 minutes: a slow clock is usually a dead battery and is not scored. | isAutoTimeOff (corroboration) | Device clock set ahead |
| Hardware identifiers that outlive the app’s identity. The SDK reads three: the Android ID, the Widevine DRM id, and the Google Services id. The Android ID resets on a factory reset; the Widevine id usually does not. Hardware seen again under a new app identity is a reset-and-reclaim. | deviceId, mediaDrmId, gsfId | Device reset or re-registered under a new identity |
| Google's own Play Integrity verdict says the device or the app is not genuine. Only a verdict that was actually decoded can fail; a missing one never does. | attested | Failed Google Play Integrity check |
How they score
This table is generated from the live scoring configuration. A signal marked “Only with other signals” adds to the score but cannot decide anything alone. How signals add up, and what each preset does, is explained on the sensitivity page.
| Signal | Weight | Strict | Balanced | Less strict |
|---|---|---|---|---|
| Android emulator | 50 | Review alone | Review alone | Review alone |
| Frida / Xposed instrumentation | 50 | Review alone | Review alone | Review alone |
| App-cloning container (Parallel Space...) | 45 | Review alone | Review alone | Only with other signals |
| Device reset or re-registered under a new identity | 45 | Review alone | Review alone | Only with other signals |
| Failed Google Play Integrity check | 45 | Review alone | Review alone | Only with other signals |
| Rooted Android device | 40 | Review alone | Review alone | Only with other signals |
| GPS-spoofing app installed | 20 | Only with other signals | Only with other signals | Only with other signals |
| App not installed from Google Play | 15 | Only with other signals | Only with other signals | Only with other signals |
| Debug build of the app | 15 | Only with other signals | Only with other signals | Only with other signals |
| Device clock set ahead | 15 | Only with other signals | Only with other signals | Only with other signals |
| VPN switched on in the device | 10 | Only with other signals | Only with other signals | Only with other signals |
Some of these also fire for honest people: a rooted enthusiast, a work profile, an app installed from a store outside the list. That is why most are weighted to matter in company, and why Play Integrity, which cannot be patched out of the APK, is the one to add.
What is collected
- Identifiers, hashed on the device. The Android ID, the Widevine id and the Google Services id are each hashed with your publishable key before they leave the phone. The raw values never reach Kaidn, and the hash is meaningless outside your account.
- Coarse device attributes. OS version, model, manufacturer, timezone, first-install time, boot count, uptime, the device clock, and hardware characteristics such as sensors, cameras, graphics version, core count and bucketed memory and storage.
- Your app’s identity. Its package name and the SHA-256 of its signing certificate, which is what the lock checks.
- The detections above, with the short evidence string behind each, and a Play Integrity token if you turned that on.
Never collected: the advertising ID, IMEI, serial number, phone number, account list, or location. The SDK does not request the location permission. The manifest lists the exact packages it looks for (root managers, GPS spoofers, app cloners) instead of asking for broad package visibility.
The Android ID is allowed for fraud prevention provided it is not joined to personal data or the resettable advertising ID, and the SDK reads neither. It differs per signing key, so it cannot be correlated across unrelated vendors. Your Play Data safety declaration is yours to make; this list is what the SDK reads.
Troubleshooting
Start with report.delivered and report.httpStatus in the callback. During integration the status is the diagnosis.
| you see | it means | fix |
|---|---|---|
httpStatus = 403 | The tracker does not list this package, or it pins a certificate this build is not signed with. | Check the package name. If you pinned a certificate, confirm it is the Play app signing certificate and that you are not running a debug build against it. |
httpStatus = 401 | The publishable key is not recognised, or has been revoked. | Copy the key from the tracker again, or make a new one. |
httpStatus = 400 | The server rejected the payload, usually an SDK older than the API schema. | Update the SDK. The reason is in report.error. |
httpStatus = 0 | The request never reached the API: DNS, TLS, a timeout, or a refused cleartext URL. | error = "cleartext_refused" means baseUrl is plain http. Only loopback is allowed: 127.0.0.1, localhost and the emulator’s 10.0.2.2. |
error = "no_device_id" | None of the three identifiers could be read, so nothing was sent. | Nothing to forward. Score the event without a device_id. |
attested = false | No Google attestation: the Integrity library is not in the app, there is no Play Services, the build is sideloaded, the device is offline, or Google throttled the request. | Expected on many devices. Check step 5 if you wanted it. The SDK waits up to eight seconds for Google, then carries on without it. |
| Integrity on, but no verdict in the dashboard | A token arrived and could not be opened: keys not saved, or the wrong ones. | Re-paste both keys on the tracker. Without working keys the beacon is accepted as unattested. |
Next steps
- Sensitivity and what gets blocked: the presets, and what each one does with these signals.
- Device identity: why an Android ID is a stronger anchor than a browser fingerprint.
- API reference: every field of
/v1/score.