---
title: "SafetyNet Is Gone. Play Integrity Is Only as Strong as Your Server"
description: "SafetyNet Attestation was turned down in January 2025. Play Integrity replaces it, but a verdict checked on the client, or at one tier, protects almost nothing."
url: https://android-x.com/blog/play-integrity-api-android
author: "Rocky Elsalaymeh"
publisher: "Strategia-X (ANDROID-ARCHITECT)"
published: 2026-04-21
updated: 2026-10-11
category: "Security"
reading_time: "10 min read"
---

# SafetyNet Is Gone. Play Integrity Is Only as Strong as Your Server

> SafetyNet Attestation was turned down in January 2025. Play Integrity replaces it, but a verdict checked on the client, or at one tier, protects almost nothing.

By Rocky Elsalaymeh. Published 2026-04-21, updated 2026-10-11. Canonical page: https://android-x.com/blog/play-integrity-api-android

## The Security Check That Quietly Stopped Working

Search your codebase for `SafetyNet.getClient`. If it is still there, you are running a security control that has been returning the same answer to every device on earth for over a year.

Google's [deprecation timeline](https://developer.android.com/privacy-and-security/safetynet/deprecation-timeline) is final: "The SafetyNet Attestation API was deprecated in 2022 and fully turned down in January 2025." It also documents what the call does now: "The attest API returns a task that always invokes the on failure listener with an ApiException and a status code of 7 (NETWORK_ERROR)." Google's [final notice](https://groups.google.com/g/safetynet-api-clients/c/qcMXTCpHReg) to API clients left no room for negotiation: "It is not possible to get an extension to use the SafetyNet Attestation API."

What happens next depends on a line of error handling someone wrote years ago. If that handler fails closed, every user has been blocked since January 2025 and you already know about it. If it fails open, and treats a network error as a reason to let the user through, which is the common choice because real network errors are common, then your attestation layer has been a no-op ever since. Nothing crashed. No dashboard went red. The abuse controls simply stopped controlling.

The replacement is the [Play Integrity API](https://developer.android.com/google/play/integrity/overview). Migrating the call is an afternoon. Migrating the *thinking* is the real work, because Play Integrity's design punishes the two habits SafetyNet let teams get away with: trusting a verdict on the device, and treating integrity as a single yes-or-no gate.

## What does a Play Integrity verdict actually tell you?

An integrity token decrypts to a JSON payload with five sections. Each answers a different question, and treating them as one boolean throws most of the signal away. From the [verdicts reference](https://developer.android.com/google/play/integrity/verdicts):

| Field | Question it answers | Values that matter |
|---|---|---|
| requestDetails | Is this token for *this* request? | Package name, requestHash or nonce, timestamp |
| appIntegrity | Is this our unmodified binary? | PLAY_RECOGNIZED, UNRECOGNIZED_VERSION, UNEVALUATED |
| deviceIntegrity | Is this a genuine, certified device? | MEETS_BASIC_INTEGRITY, MEETS_DEVICE_INTEGRITY, MEETS_STRONG_INTEGRITY |
| accountDetails | Did this user get the app from Play? | LICENSED, UNLICENSED, UNEVALUATED |
| environmentDetails | Is anything watching or driving the app? | App access risk (capturing, controlling, overlays), Play Protect verdict |

`PLAY_RECOGNIZED` means "the app and certificate match the versions distributed by Google Play." `UNRECOGNIZED_VERSION` means "the certificate or package name does not match Google Play records", the signature of a repackaged or tampered build. `UNLICENSED` covers the sideload case: "the user sideloads your app or doesn't acquire it from Google Play."

The environment signals are the most underused. `appAccessRiskVerdict` reports, for example, when "there are apps running that have permissions enabled that could be used to view the screen while your app is running", which is exactly the precondition for overlay and screen-scraping fraud in a banking or payments flow.

Google's claim for the payoff is strong: in a [November 2025 post](https://android-developers.googleblog.com/2025/10/stronger-threat-detection-simpler.html) it reported that "apps using Play integrity features see 80% lower unauthorized usage on average compared to other apps." That figure is Google's own and it is an average; the architecture below is what decides whether your app lands above or below it.

## Standard or Classic: Pick by Frequency, Not by Habit

Play Integrity offers two request types, and the [overview](https://developer.android.com/google/play/integrity/overview) is explicit about the trade: "Standard requests have the lowest latency (a few hundred milliseconds on average) and a high reliability of obtaining a usable verdict." Classic requests "have higher latency (a few seconds on average) and you are responsible for mitigating the risk of certain types of attacks."

| | Standard request | Classic request |
|---|---|---|
| Latency | A few hundred ms on average | A few seconds on average |
| Setup | Warm up a token provider first | None |
| Request binding | `requestHash` of the action | `nonce` you generate server-side |
| Replay protection | Automatic | Yours to implement |
| Use it for | Any action, on demand | Occasional, high-value checks |

Standard requests need a one-time warm-up: you must prepare, or warm up, the integrity token provider first, and Google notes the "typical warm up latency is a few seconds and the majority of all warm ups are under 10s." Do it at app start or when the user enters the flow that will need it, never on the tap that needs the answer.

The `requestHash` is not optional in practice. The [standard request guide](https://developer.android.com/google/play/integrity/standard) warns: "Without the requestHash, the integrity token will be bound only to the device, but not to the specific request, which opens up the possibility of attack." Hash the parameters of the action you are protecting (the transfer amount and recipient, not just a session id), so a token minted for one request cannot be replayed onto another.

```kotlin
class IntegrityClient(context: Context, private val cloudProjectNumber: Long) {
    private val manager = IntegrityManagerFactory.createStandard(context)
    private var provider: StandardIntegrityTokenProvider? = null

    // Warm up early: typically a few seconds, most under 10s.
    suspend fun warmUp() {
        provider = manager.prepareIntegrityToken(
            PrepareIntegrityTokenRequest.builder()
                .setCloudProjectNumber(cloudProjectNumber)
                .build()
        ).await()
    }

    // Bind the token to THIS action: hash what the server will act on.
    suspend fun tokenFor(action: TransferRequest): String {
        val requestHash = sha256("${action.fromAccount}|${action.toAccount}|${action.amountMinor}")
        val response = checkNotNull(provider) { "warmUp() first" }.request(
            StandardIntegrityTokenRequest.builder()
                .setRequestHash(requestHash)
                .build()
        ).await()
        return response.token() // opaque: send it to the server, never decode it here
    }
}
```

## The Device Verdict Changed Meaning Under You

Teams that migrated early should re-read their policy, because the most-used verdict no longer means what it meant when the policy was written. In [December 2024](https://android-developers.googleblog.com/2024/12/making-play-integrity-api-faster-resilient-private.html) Google moved device verdicts on Android 13 and higher onto hardware-backed signals and announced that "all API integrations will automatically transition to the new verdicts in May 2025." The same post quantified the benefit: signals collected and evaluated server-side dropped by "~90%" and "verdict latency can improve by up to ~80%."

The definitions changed with it. Per the [current reference](https://developer.android.com/google/play/integrity/verdicts), on Android 13 and higher:

- **MEETS_STRONG_INTEGRITY** "requires MEETS_DEVICE_INTEGRITY and security updates in the last year for all partitions of the device, including an Android OS partition patch and a vendor partition patch."
- **MEETS_DEVICE_INTEGRITY** means "there is hardware-backed proof that the device bootloader is locked and the loaded Android OS is a certified device manufacturer image."
- **MEETS_BASIC_INTEGRITY** "requires only that the attestation root of trust is provided by Google."

On Android 12 and lower, strong integrity "does not require the device to have a recent security update." So the same label means different things on different OS versions, and the strong verdict is now partly a statement about patch recency. A policy that demands `MEETS_STRONG_INTEGRITY` for sign-in no longer just blocks rooted phones. It blocks anyone whose manufacturer stopped shipping patches more than a year ago. That may be the right call for a wire transfer. It is the wrong call for reading a news feed.

## Decide on the Server, and Decide in Tiers

Every serious Play Integrity failure has the same root cause: the decision was made where the attacker lives. Google's [setup guide](https://developer.android.com/google/play/integrity/setup) says it in one line: "Never decrypt tokens or expose keys within your client app." The client's only job is to obtain an opaque token and forward it. Your server decrypts it (Google recommends decrypting on Google's servers; self-managed keys are supported), checks that `requestDetails` matches the request it is serving, and only then reads the verdicts.

The second failure is binary enforcement. Google's [overview](https://developer.android.com/google/play/integrity/overview) recommends graded responses instead, "a series of related responses such as Allow, Allow with limits, Allow with limits after CAPTCHA completion, and Deny." A graded policy survives the verdict drift described above and degrades gracefully for the long tail of legitimate users on old or unusual hardware, instead of turning a security control into a support-ticket generator.

Integrity is one input to that decision. Who the user is comes from authentication, and [passkeys through Credential Manager](https://android-x.com/blog/credential-manager-passkeys-android) are the phishing-resistant way to establish it.

```kotlin
// Server side, after decoding the token through Google's decodeIntegrityToken endpoint.
fun decide(payload: IntegrityPayload, expected: ExpectedRequest, action: Action): Decision {
    val req = payload.requestDetails
    if (req.requestPackageName != expected.packageName) return Decision.Deny
    if (req.requestHash != expected.requestHash) return Decision.Deny          // replayed or swapped
    if (expected.now - req.timestampMillis > 60_000) return Decision.Deny      // stale token

    val genuineApp = payload.appIntegrity.appRecognitionVerdict == "PLAY_RECOGNIZED"
    val labels = payload.deviceIntegrity.deviceRecognitionVerdict.orEmpty()
    val screenWatched = payload.environmentDetails?.appAccessRiskVerdict
        ?.appsDetected.orEmpty().any { it.endsWith("_CAPTURING") }

    return when {
        !genuineApp -> Decision.Deny
        action.isHighValue && "MEETS_STRONG_INTEGRITY" !in labels -> Decision.StepUp   // re-auth, limits
        action.isHighValue && screenWatched -> Decision.StepUp
        "MEETS_DEVICE_INTEGRITY" in labels -> Decision.Allow
        "MEETS_BASIC_INTEGRITY" in labels -> Decision.AllowWithLimits
        else -> Decision.Challenge                                               // CAPTCHA, then limits
    }
}
```

## The Operational Details That Bite in Production

Three platform facts belong in the design review, not the incident review.

**Quota is shared and finite.** "Your app or SDK has a default daily limit of 10,000 total requests, tied to the associated Cloud Project Number," and the [setup guide](https://developer.android.com/google/play/integrity/setup) notes that quota increases "apply to both client-side token generation and server-side decryption calls." A popular app that requests a token on every screen will exhaust the default on its first busy day. Request on protected actions, and file the quota increase before launch, not during it.

**Users can often fix a failing verdict themselves.** Play Integrity ships [remediation dialogs](https://developer.android.com/google/play/integrity/remediation) shown through `showDialog()`: `GET_LICENSED` for a sideloaded install, and `GET_INTEGRITY` / `GET_STRONG_INTEGRITY` for device problems, which "requires the Integrity API Android library version 1.5.0 or higher." A denial that offers a fix converts a support ticket into a retry.

**Repeat abuse survives a factory reset, and now so can your memory of it.** [Device recall](https://developer.android.com/google/play/integrity/device-recall), announced as a public beta at [Google I/O 2025](https://android-developers.googleblog.com/2025/05/io-2025-whats-new-in-google-play.html), lets you "read three custom values or bits for each device", stored on Google's servers "even after your app is reinstalled or the device is reset." Three bits are enough to record that a device already claimed the sign-up bonus, without collecting a device identifier.

The migration checklist, in order: delete every SafetyNet call and its fail-open branch; move all decryption and decisions to the server; bind every token to its action with `requestHash`; replace the single gate with graded responses; and wire the remediation dialogs before you turn enforcement on.

For where integrity checks sit among the other device-side controls, see [Android security practices](https://android-x.com/blog/android-security-best-practices).

## Key takeaways

- SafetyNet Attestation was fully turned down in January 2025; every attest call now fails with a network error, so a fail-open handler means zero protection.
- A Play Integrity verdict has five parts (request, app, device, account, environment); collapsing them into one boolean discards most of the signal.
- Use standard requests with a warm-up and a requestHash bound to the protected action; reserve classic requests and nonces for occasional high-value checks.
- On Android 13 and higher, MEETS_STRONG_INTEGRITY now requires a security update within the last year, so it is partly a patch-recency check.
- Decrypt and decide on the server, never in the client, and respond in tiers: allow, allow with limits, challenge, deny.
- Plan for the 10,000 requests/day default quota, ship the remediation dialogs, and consider device recall for repeat-abuse detection.

## Frequently asked questions

### Does SafetyNet Attestation still work?

No. Google's deprecation timeline says the SafetyNet Attestation API was deprecated in 2022 and fully turned down in January 2025. The attest call now always fails with an ApiException and status code 7, NETWORK_ERROR. Code that treats that error as a pass has been checking nothing since then. Migrate to the Play Integrity API.

### Can I check the Play Integrity verdict inside the app?

No. Send the opaque token to your server, decrypt and verify it there, and make the decision there. A verdict evaluated inside the app can be patched out by anyone who modifies the app, which is exactly the person the check exists to stop. The app only requests the token and passes it on.

### Should I require MEETS_STRONG_INTEGRITY for every user?

Usually not. On Android 13 and higher the strong verdict also requires a security update within the last year, so it excludes genuine devices whose manufacturers stopped shipping patches. Reserve it for high-value actions such as a transfer, and respond in tiers elsewhere: allow, add friction or block, depending on the verdict.

## Related articles

- [Securing Android Apps: OWASP Mobile Top 10 in Practice](https://android-x.com/blog/android-security-best-practices.md)
- [Implementing Passkeys and Biometric Auth in Android with Credential Manager](https://android-x.com/blog/credential-manager-passkeys-android.md)
- [Offline-First Android Architecture with Room and WorkManager](https://android-x.com/blog/offline-first-android-architecture.md)
