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: { ... }
}

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 prepared (computed on the server).
  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.
  // 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 requestDetails = JSONObject(payload).getJSONObject("appIntegrity")
val appRecognitionVerdict = requestDetails.getString("appRecognitionVerdict")

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

Java

JSONObject requestDetails =
    new JSONObject(payload).getJSONObject("appIntegrity");
String appRecognitionVerdict =
    requestDetails.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 have one of the following labels:

MEETS_DEVICE_INTEGRITY
The app is running on an Android device powered by Google Play services. The device passes system integrity checks and meets Android compatibility requirements.
No labels (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.

If you opt in to receive additional labels in the integrity verdict, deviceRecognitionVerdict can have 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 device powered by 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.

Furthermore, if your app is being released to approved emulators, the deviceRecognitionVerdict can also take on the following label:

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

Account details field

The accountDetails field contains a single value, appLicensingVerdict, that represents app licensing/entitlement status.

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

appLicensingVerdict can have the following values:

LICENSED
The user has an app entitlement. In other words, the user installed or bought your app on Google Play.
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.
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 requestDetails = JSONObject(payload).getJSONObject("accountDetails")
val appLicensingVerdict = requestDetails.getString("appLicensingVerdict")

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

Java

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

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