Play Integrity API (successor to SafetyNet Attestation) lets your server verify that a request comes from a genuine, unmodified version of your app running on a certified Android device. It protects against bot attacks, cheating, and API abuse.
What Play Integrity Verifies
A Play Integrity token contains three verdicts:
- App integrity: Is this a genuine, unmodified APK from Google Play?
- Device integrity: Is this device certified (passes CTS), not rooted, not an emulator?
- Account details: Is the user's Google account licensed for this app?
Requesting a Token
// implementation("com.google.android.play:integrity:1.3.0")
class IntegrityChecker(private val context: Context) {
private val integrityManager = IntegrityManagerFactory.create(context)
suspend fun getIntegrityToken(requestHash: String): String = suspendCoroutine { cont ->
val request = IntegrityTokenRequest.newBuilder()
.setNonce(requestHash) // should be a hash of the request content
.build()
integrityManager.requestIntegrityToken(request)
.addOnSuccessListener { response ->
cont.resume(response.token())
}
.addOnFailureListener { exception ->
cont.resumeWithException(exception)
}
}
}
Server-Side Verification
The token is opaque — it must be sent to your server for verification via the Play Integrity API:
Client:
1. Generate nonce = SHA256(request_body + timestamp + user_id)
2. Request token with this nonce
3. Send API request + token to your server
Server:
1. POST token to: https://playintegrity.googleapis.com/v1/{package}:decodeIntegrityToken
2. Parse verdicts: appIntegrity, deviceIntegrity, accountDetails
3. Verify nonce matches the request (anti-replay)
4. Accept or reject based on verdict thresholds
// Retrofit interface for sending token to your backend
interface ApiService {
@POST("api/protected-action")
suspend fun performAction(
@Body request: ActionRequest
): ActionResponse
}
data class ActionRequest(
val actionData: String,
val integrityToken: String, // Play Integrity token
val nonce: String // client-generated nonce
)
// Usage in ViewModel
class PurchaseViewModel(
private val integrityChecker: IntegrityChecker,
private val apiService: ApiService
) : ViewModel() {
fun purchaseItem(itemId: String) = viewModelScope.launch {
val requestData = "purchase:$itemId:${System.currentTimeMillis()}"
val nonce = sha256(requestData)
val token = try {
integrityChecker.getIntegrityToken(nonce)
} catch (e: IntegrityServiceException) {
handleError(e); return@launch
}
apiService.performAction(ActionRequest(requestData, token, nonce))
}
}
Verdicts and What They Mean
APP_INTEGRITY:
PLAY_RECOGNIZED — genuine, non-tampered app from Google Play ✅
UNRECOGNIZED_VERSION — side-loaded APK or unreleased version ⚠️
UNEVALUATED — not enough information ⚠️
DEVICE_INTEGRITY:
MEETS_DEVICE_INTEGRITY — CTS certified device ✅
MEETS_BASIC_INTEGRITY — passes basic but not CTS (custom ROM) ⚠️
(empty) — failed all checks ❌
ACCOUNT_DETAILS:
LICENSED — user's Google account licensed this app ✅
UNLICENSED — not licensed (pirated) ❌
UNEVALUATED — couldn't verify ⚠️
Choosing Your Enforcement Level
Not all use cases warrant the same strictness:
// Server-side policy (pseudocode)
fun evaluateIntegrity(verdict: IntegrityVerdict): Action {
return when {
// Strict: require full integrity for financial operations
verdict.appIntegrity != "PLAY_RECOGNIZED" ||
verdict.deviceIntegrity.isEmpty() ->
Action.BLOCK
// Medium: allow degraded experience for basic integrity
verdict.deviceIntegrity == "MEETS_BASIC_INTEGRITY" && !isFinancialAction ->
Action.ALLOW_WITH_CAPTCHA
// Allow: passes all checks
else -> Action.ALLOW
}
}
Important Limitations
- Rate limits: Play Integrity has per-app daily limits (~10,000 tokens/day by default; request quota increase for higher volume)
- Latency: Token requests can take 1-10 seconds; don't block UI on them — request proactively
- Not a silver bullet: determined attackers can root with hidden root detectors; Play Integrity is one layer of defense
- Nonce anti-replay: always bind the nonce to the specific request; a static nonce can be replayed
SafetyNet vs Play Integrity
SafetyNet Attestation is deprecated (was shut down in 2024). Play Integrity API is the replacement with stronger guarantees and better server-side verification flow.
Key Takeaways
| Concept | Rule |
|---|---|
| Token purpose | Verify app genuineness and device integrity server-side |
| Nonce | Always bind to request content — prevents token replay |
| Server verification | Token must be verified server-side; don't trust client-side parsing |
| Enforcement | Match strictness to risk; block only for high-value operations |
| Rate limits | Request tokens proactively; cache within a short window (< 1 min) |
| Not a root check | Cannot fully block rooted devices; use as one layer among many |