Integrity verdicts

This page describes how to interpret and work with the returned integrity verdict. Whether you make a standard or classic API request, the integrity verdict is returned in the same format with similar content. The integrity verdict communicates information about the validity of devices, apps, and accounts. Your app's server can use the resulting payload in a decrypted, verified verdict to determine how best to proceed with a particular action or request in your app.

Returned integrity verdict format

The payload is plain-text JSON and contains integrity signals alongside developer-provided information.

The general payload structure is as follows:

{
  requestDetails: { ... }
  appIntegrity: { ... }
  deviceIntegrity: { ... }
  accountDetails: { ... }
  environmentDetails: { ... }
}

You must first check that the values in the requestDetails field match those of the original request before checking each integrity verdict. The following sections describe each field in more detail.

Request details field

The requestDetails field contains information about the request, including developer-provided information in the requestHash for standard requests and the nonce for classic requests.

For standard API requests:

requestDetails: {
  // Application package name this attestation was requested for.
  // Note that this field might be spoofed in the middle of the request.
  requestPackageName: "com.package.name"
  // Request hash provided by the developer.
  requestHash: "aGVsbG8gd29scmQgdGhlcmU"
  // The timestamp in milliseconds when the integrity token
  // was requested.
  timestampMillis: "1675655009345"
}

These values should match those of the original request. Therefore, verify the requestDetails part of the JSON payload by making sure that the requestPackageName and requestHash match what was sent in the original request, as shown in the following code snippet:

Kotlin

val requestDetails = JSONObject(payload).getJSONObject("requestDetails")
val requestPackageName = requestDetails.getString("requestPackageName")
val requestHash = requestDetails.getString("requestHash")
val timestampMillis = requestDetails.getLong("timestampMillis")
val currentTimestampMillis = ...

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request
    || !requestHash.equals(expectedRequestHash)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

Java

RequestDetails requestDetails =
    decodeIntegrityTokenResponse
    .getTokenPayloadExternal()
    .getRequestDetails();
String requestPackageName = requestDetails.getRequestPackageName();
String requestHash = requestDetails.getRequestHash();
long timestampMillis = requestDetails.getTimestampMillis();
long currentTimestampMillis = ...;

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request.
    || !requestHash.equals(expectedRequestHash)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

For classic API requests:

requestDetails: {
  // Application package name this attestation was requested for.
  // Note that this field might be spoofed in the middle of the
  // request.
  requestPackageName: "com.package.name"
  // base64-encoded URL-safe no-wrap nonce provided by the developer.
  nonce: "aGVsbG8gd29scmQgdGhlcmU"
  // The timestamp in milliseconds when the request was made
  // (computed on the server).
  timestampMillis: "1617893780"
}

These values should match those of the original request. Therefore, verify the requestDetails part of the JSON payload by making sure that the requestPackageName and nonce match what was sent in the original request, as shown in the following code snippet:

Kotlin

val requestDetails = JSONObject(payload).getJSONObject("requestDetails")
val requestPackageName = requestDetails.getString("requestPackageName")
val nonce = requestDetails.getString("nonce")
val timestampMillis = requestDetails.getLong("timestampMillis")
val currentTimestampMillis = ...

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request. See 'Generate a nonce'
        // section of the doc on how to store/compute the expected nonce.
    || !nonce.equals(expectedNonce)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

Java

JSONObject requestDetails =
    new JSONObject(payload).getJSONObject("requestDetails");
String requestPackageName = requestDetails.getString("requestPackageName");
String nonce = requestDetails.getString("nonce");
long timestampMillis = requestDetails.getLong("timestampMillis");
long currentTimestampMillis = ...;

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request. See 'Generate a nonce'
        // section of the doc on how to store/compute the expected nonce.
    || !nonce.equals(expectedNonce)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

Application integrity field

The appIntegrity field contains package-related information.

appIntegrity: {
  // PLAY_RECOGNIZED, UNRECOGNIZED_VERSION, or UNEVALUATED.
  appRecognitionVerdict: "PLAY_RECOGNIZED"
  // The package name of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  packageName: "com.package.name"
  // The sha256 digest of app certificates (base64-encoded URL-safe).
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  certificateSha256Digest: ["6a6a1474b5cbbb2b1aa57e0bc3"]
  // The version of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  versionCode: "42"
}

appRecognitionVerdict can have the following values:

PLAY_RECOGNIZED
The app and certificate match the versions distributed by Google Play.
UNRECOGNIZED_VERSION
The certificate or package name does not match Google Play records.
UNEVALUATED
Application integrity was not evaluated. A necessary requirement was missed, such as the device not being trustworthy enough.

To ensure that the token was generated by an app that was created by you, verify that the application integrity is as expected, as shown in the following code snippet:

Kotlin

val appIntegrity = JSONObject(payload).getJSONObject("appIntegrity")
val appRecognitionVerdict = appIntegrity.getString("appRecognitionVerdict")

if (appRecognitionVerdict == "PLAY_RECOGNIZED") {
    // Looks good!
}

Java

JSONObject appIntegrity =
    new JSONObject(payload).getJSONObject("appIntegrity");
String appRecognitionVerdict =
    appIntegrity.getString("appRecognitionVerdict");

if (appRecognitionVerdict.equals("PLAY_RECOGNIZED")) {
    // Looks good!
}

You can also check the app package name, app version, and app certificates manually.

Device integrity field

The deviceIntegrity field can contain a single value, deviceRecognitionVerdict, that has one or more labels representing how well a device can enforce app integrity. If a device does not meet the criteria of any labels, the deviceIntegrity field is empty.

deviceIntegrity: {
  // "MEETS_DEVICE_INTEGRITY" is one of several possible values.
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
}

By default, deviceRecognitionVerdict can contain the following:

MEETS_DEVICE_INTEGRITY
The app is running on an Android-powered device with Google Play services. The device passes system integrity checks and meets Android compatibility requirements.
Empty (a blank value)
The app is running on a device that has signs of attack (such as API hooking) or system compromise (such as being rooted), or the app is not running on a physical device (such as an emulator that does not pass Google Play integrity checks).

To ensure that the token came from a trustworthy device, verify the deviceRecognitionVerdict is as expected, as shown in the following code snippet:

Kotlin

val deviceIntegrity =
    JSONObject(payload).getJSONObject("deviceIntegrity")
val deviceRecognitionVerdict =
    if (deviceIntegrity.has("deviceRecognitionVerdict")) {
        deviceIntegrity.getJSONArray("deviceRecognitionVerdict").toString()
    } else {
        ""
    }

if (deviceRecognitionVerdict.contains("MEETS_DEVICE_INTEGRITY")) {
    // Looks good!
}

Java

JSONObject deviceIntegrity =
    new JSONObject(payload).getJSONObject("deviceIntegrity");
String deviceRecognitionVerdict =
    deviceIntegrity.has("deviceRecognitionVerdict")
    ? deviceIntegrity.getJSONArray("deviceRecognitionVerdict").toString()
    : "";

if (deviceRecognitionVerdict.contains("MEETS_DEVICE_INTEGRITY")) {
    // Looks good!
}

If you are having problems with your testing device meeting device integrity, make sure the factory ROM is installed (for example, by resetting the device) and that the bootloader is locked. You can also create Play Integrity API tests in your Play Console.

Conditional device labels

If your app is being released to Google Play Games for PC, the deviceRecognitionVerdict can also contain the following label:

MEETS_VIRTUAL_INTEGRITY
The app is running on an Android-powered emulator with Google Play services. The emulator passes system integrity checks and meets core Android compatibility requirements.

Optional device information

If you opt in to receive additional labels in the integrity verdict, deviceRecognitionVerdict can contain the following additional labels:

MEETS_BASIC_INTEGRITY
The app is running on a device that passes basic system integrity checks. The device may not meet Android compatibility requirements and may not be approved to run Google Play services. For example, the device may be running an unrecognized version of Android, may have an unlocked bootloader, or may not have been certified by the manufacturer.
MEETS_STRONG_INTEGRITY
The app is running on an Android-powered device with Google Play services and has a strong guarantee of system integrity such as a hardware-backed proof of boot integrity. The device passes system integrity checks and meets Android compatibility requirements.

A single device will return multiple device labels in the device integrity verdict if each of the label's criteria is met.

Recent device activity

You can also opt-in to recent device activity, which tells you how many times your app requested an integrity token on a specific device in the last hour. You can use recent device activity to protect your app against unexpected, hyperactive devices that could be an indication of an active attack. You can decide how much to trust each recent device activity level based on how many times you expect your app installed on a typical device to request an integrity token each hour.

If you opt-in to receive recentDeviceActivity the deviceIntegrity field will have two values:

deviceIntegrity: {
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
  recentDeviceActivity: {
    // "LEVEL_2" is one of several possible values.
    deviceActivityLevel: "LEVEL_2"
  }
}

The deviceActivityLevel definitions differ between modes and can have one of the following values:

Recent device activity level Standard API integrity token requests on this device in the last hour per app Classic API integrity token requests on this device in the last hour per app
LEVEL_1 (lowest) 10 or fewer 5 or fewer
LEVEL_2 Between 11 and 25 Between 6 and 10
LEVEL_3 Between 26 and 50 Between 11 and 15
LEVEL_4 (highest) More than 50 More than 15
UNEVALUATED Recent device activity was not evaluated. This could happen because:
  • The device is not trustworthy enough.
  • The version of your app installed on the device is unknown to Google Play.
  • Technical issues on the device.

Account details field

The accountDetails field contains a single value, appLicensingVerdict, that represents app's Google Play licensing status for the user account that's signed in on the device. If the user account has the Play license for the app, that means they downloaded it or bought it from Google Play.

accountDetails: {
  // This field can be LICENSED, UNLICENSED, or UNEVALUATED.
  appLicensingVerdict: "LICENSED"
}

appLicensingVerdict can have one of the following values:

LICENSED
The user has an app entitlement. In other words, the user installed or updated your app from Google Play on their device.
UNLICENSED
The user doesn't have an app entitlement. This happens when, for example, the user sideloads your app or doesn't acquire it from Google Play. You can show the GET_LICENSED dialog to users to remedy this.
UNEVALUATED

Licensing details were not evaluated because a necessary requirement was missed.

This could happen for several reasons, including the following:

  • The device is not trustworthy enough.
  • The version of your app installed on the device is unknown to Google Play.
  • The user is not signed in to Google Play.

To check that the user has an app entitlement for your app, verify that the appLicensingVerdict is as expected, as shown in the following code snippet:

Kotlin

val accountDetails = JSONObject(payload).getJSONObject("accountDetails")
val appLicensingVerdict = accountDetails.getString("appLicensingVerdict")

if (appLicensingVerdict == "LICENSED") {
    // Looks good!
}

Java

JSONObject accountDetails =
    new JSONObject(payload).getJSONObject("accountDetails");
String appLicensingVerdict = accountDetails.getString("appLicensingVerdict");

if (appLicensingVerdict.equals("LICENSED")) {
    // Looks good!
}

Environment details field

You can also opt in to additional signals about the environment. App access risk tells your app if there are other apps running that could be used to capture the screen, display overlays, or control the device. The Play Protect verdict tells you if Google Play Protect is enabled on the device and whether it has found known malware.

If you have opted in to the App Access Risk verdict or the Play Protect verdict in your Google Play Console, then your API response will include the environmentDetails field. The environmentDetails field can contain two values, appAccessRiskVerdict and playProtectVerdict.

App access risk verdict (beta)

Once enabled, the environmentDetails field in the Play Integrity API payload will contain the new app access risk verdict.

{
  requestDetails: { ... }
  appIntegrity: { ... }
  deviceIntegrity: { ... }
  accountDetails: { ... }
  environmentDetails: {
      appAccessRiskVerdict: {
          // This field contains one or more responses, for example the following.
          appsDetected: ["KNOWN_INSTALLED", "UNKNOWN_INSTALLED", "UNKNOWN_CAPTURING"]
      }
 }
}

If app access risk was evaluated, appAccessRiskVerdict contains the field appsDetected with one or more responses. These responses fall into one of the following two groups depending on the install source of the detected apps:

  • Play or system apps: Apps that are installed by Google Play or preloaded by the device manufacturer on the device's system partition (identified with FLAG_SYSTEM). Responses for such apps are prefixed by KNOWN_.

  • Other apps: Apps that are not installed by Google Play. This excludes apps preloaded on the system partition by the device manufacturer. Responses for such apps are prefixed by UNKNOWN_.

The following responses can be returned:

KNOWN_INSTALLED, UNKNOWN_INSTALLED
There are apps installed that match the corresponding install source.
KNOWN_CAPTURING, UNKNOWN_CAPTURING
There are apps running that have permissions enabled that could be used to view the screen while your app is running. This excludes any verified accessibility services known to Google Play running on the device.
KNOWN_CONTROLLING, UNKNOWN_CONTROLLING
There are apps running that have permissions enabled that could be used to control the device and directly control inputs into your app and could be used to capture inputs and outputs of your app. This excludes any verified accessibility services known to Google Play running on the device.
KNOWN_OVERLAYS, UNKNOWN_OVERLAYS
There are apps running that have permissions enabled that could be used to display overlays on your app. This excludes any verified accessibility services known to Google Play running on the device.
EMPTY (a blank value)

App access risk is not evaluated if a necessary requirement was missed. In this case the appAccessRiskVerdict field is empty. This could happen for several reasons, including the following:

  • The device is not trustworthy enough.
  • The device form factor is not a phone, tablet, or foldable.
  • The device is not running Android 6 (API level 23) or higher.
  • The version of your app installed on the device is unknown to Google Play.
  • The version of the Google Play Store on the device is outdated.
  • Games only: The user account does not have a Play license for the game.
  • A standard request was used with the verdictOptOut parameter.
  • A standard request was used with a Play Integrity API library version that doesn't yet support app access risk for standard requests.

App access risk automatically excludes verified accessibility services that have been through an enhanced Google Play accessibility review (installed by any app store on the device). "Excluded" means that verified accessibility services running on the device won't return a capturing, controlling, or overlays response in the app access risk verdict. To request an enhanced Google Play accessibility review for your accessibility app, publish it on Google Play ensuring that your app has the isAccessibilityTool flag set to true in your app's manifest, or request a review.

The following table gives some examples of verdicts and what they mean (this table does not list every possible result):

Example app access risk verdict response Interpretation
appsDetected:
["KNOWN_INSTALLED"]
There are only apps installed that are recognized by Google Play or preloaded on the system partition by the device manufacturer.
There are no apps running that would result in the capturing, controlling, or overlays verdicts.
appsDetected:
["KNOWN_INSTALLED",
"UNKNOWN_INSTALLED",
"UNKNOWN_CAPTURING"]
There are apps installed by Google Play or preloaded on the system partition by the device manufacturer.
There are other apps running and have permissions enabled that could be used to view the screen or capture other inputs and outputs.
appsDetected:
["KNOWN_INSTALLED",
"KNOWN_CAPTURING",
"UNKNOWN_INSTALLED",
"UNKNOWN_CONTROLLING"]
There are Play or system running that have permissions enabled that could be used to view the screen or capture other inputs and outputs.
There are also other apps running that have permissions enabled that could be used to control the device and directly control inputs into your app.
appAccessRiskVerdict: {} App access risk is not evaluated because a necessary requirement was missed. For example, the device was not trustworthy enough.

Depending on your risk level, you can decide what combination of verdicts are acceptable to proceed and what verdicts you want to take action on. The following code snippet illustrates an example of verifying that there are no apps running that could capture the screen or control your app:

Kotlin

val environmentDetails =
    JSONObject(payload).getJSONObject("environmentDetails")
val appAccessRiskVerdict =
    environmentDetails.getJSONObject("appAccessRiskVerdict")

if (appAccessRiskVerdict.has("appsDetected")) {
    val appsDetected = appAccessRiskVerdict.getJSONArray("appsDetected").toString()
    if (!appsDetected.contains("CAPTURING") && !appsDetected.contains("CONTROLLING")) {
        // Looks good!
    }
}

Java

JSONObject environmentDetails =
    new JSONObject(payload).getJSONObject("environmentDetails");
JSONObject appAccessRiskVerdict =
    environmentDetails.getJSONObject("appAccessRiskVerdict");

if (appAccessRiskVerdict.has("appsDetected")) {
    String appsDetected = appAccessRiskVerdict.getJSONArray("appsDetected").toString()
    if (!appsDetected.contains("CAPTURING") && !appsDetected.contains("CONTROLLING")) {
        // Looks good!
    }
}
Remediate app access risk verdicts

Depending on your risk level, you can decide what app access risk verdicts you want to take action on before letting the user complete a request or action. There are optional Google Play prompts you can show to the user after checking the app access risk verdict. You can show CLOSE_UNKNOWN_ACCESS_RISK to ask the user to close unknown apps causing the app access risk verdict or you can show CLOSE_ALL_ACCESS_RISK to ask the user to close all apps (known and unknown) causing the app access risk verdict.

Play Protect verdict

Once enabled, the environmentDetails field in the Play Integrity API payload will contain the Play Protect verdict:

environmentDetails: {
  playProtectVerdict: "NO_ISSUES"
}

playProtectVerdict can have one of the following values:

NO_ISSUES
Play Protect is turned on and did not find any app issues on the device.
NO_DATA
Play Protect is turned on but no scan has been performed yet. The device or the Play Store app may have been recently reset.
POSSIBLE_RISK
Play Protect is turned off.
MEDIUM_RISK
Play Protect is turned on and has found potentially harmful apps installed on the device.
HIGH_RISK
Play Protect is turned on and has found dangerous apps installed on the device.
UNEVALUATED

The Play Protect verdict was not evaluated.

This could happen for several reasons, including the following:

  • The device is not trustworthy enough.
  • Games only: The user account does not have a Play license for the game.

Guidance on using the Play Protect verdict

Your app's backend server can decide how to act based on the verdict based on your risk tolerance. Here are some suggestions and potential user actions:

NO_ISSUES
Play Protect is on and hasn't found any issues so no user action is required.
POSSIBLE_RISK and NO_DATA
When receiving these verdicts, ask the user to check that Play Protect is on and has performed a scan. NO_DATA should appear only in rare circumstances.
MEDIUM_RISK and HIGH_RISK
Depending on your risk tolerance, you can ask the user to launch Play Protect and take action on the Play Protect warnings. If the user can't fufill these requirements you could block them from the server action.