Make a classic API request

If you're only planning to make standard API requests, which are suitable for the majority of developers, you can skip ahead to integrity verdicts. This page describes making classic API requests for integrity verdicts, which are supported on Android 4.4 (API level 19) or higher.

Considerations

Compare standard and classic requests

You can make standard requests, classic requests, or a combination of the two depending on your app's security and anti-abuse needs. Standard requests are suitable for all apps and games and can be used to check that any action or server call is genuine, while delegating some protection against replayability and exfiltration to Google Play. Classic requests are more expensive to make and you are responsible for correctly implementing them to protect against exfiltration and certain types of attacks. Classic requests should be made less frequently than standard requests, for example as an occasional one-off to check if a highly valuable or sensitive action is genuine.

The following table highlights the key differences between the two types of requests:

Standard API request Classic API request
Pre-requisites
Minimum Android SDK version required Android 5.0 (API level 21) or higher Android 4.4 (API level 19) or higher
Google Play requirements Google Play Store and Google Play services Google Play Store and Google Play services
Integration details
API warm up required ✔️ (a few seconds)
Typical request latency A few hundred milliseconds A few seconds
Potential request frequency Frequent (on-demand check for any action or request) Infrequent (one-off check for highest value actions or most sensitive requests)
Timeouts Most warm ups are under 10s but they involve a server call, so a long timeout is recommended (e.g. 1 minute). Verdict requests happen client-side Most requests are under 10s but they involve a server call, so a long timeout is recommended (e.g. 1 minute)
Integrity verdict token
Contains device, app, and account details ✔️ ✔️
Token caching Protected on-device caching by Google Play Not recommended
Decrypt and verify token via Google Play server ✔️ ✔️
Typical decryption server-to-server request latency 10s of milliseconds with three-nines availability 10s of milliseconds with three-nines availability
Decrypt and verify token locally in a secure server environment ✔️
Decrypt and verify token client-side
Integrity verdict freshness Some automatic caching and refreshing by Google Play All verdicts recomputed on each request
Limits
Requests per app per day 10,000 by default (an increase can be requested) 10,000 by default (an increase can be requested)
Requests per app instance per minute Warm ups: 5 per minute
Integrity tokens: No public limit*
Integrity tokens: 5 per minute
Protection
Mitigate against tampering and similar attacks Use requestHash field Use nonce field with content binding based on request data
Mitigate against replay and similar attacks Automatic mitigation by Google Play Use nonce field with server side logic

* All requests, including those without public limits, are subject to non-public defensive limits at high values

Make classic requests infrequently

Generating an integrity token uses time, data, and battery, and each app has a maximum number of classic requests it can make per day. Therefore you should only make classic requests to check the highest value or most sensitive actions are genuine when you want an additional guarantee to a standard request. You shouldn't make classic requests for high-frequency or low-value actions. Don't make classic requests every time the app goes to the foreground nor every few minutes in the background, and avoid calling from a large number of devices at the same time. An app making too many classic requests calls may be throttled to protect users from incorrect implementations.

Avoid caching verdicts

Caching a verdict increases the risk of attacks such as exfiltration and replay, where a good verdict is reused from an untrusted environment. If you're considering making a classic request and then caching it for use later, it's recommended instead to perform a standard request on demand. Standard requests involve some caching on the device but Google Play uses additional protection techniques to mitigate the risk of replay attacks and exfiltration.

Use the nonce field to protect classic requests

The Play Integrity API offers a field called nonce, which can be used to further protect your app against certain attacks, such as replay and tampering attacks. The Play Integrity API returns the value you set in this field, inside the signed integrity response. Carefully follow the guidance on how to generate nonces to protect your app from attacks.

Retry classic requests with exponential backoff

Environmental conditions, such as an unstable Internet connection or an overloaded device, can cause device integrity checks to fail. This can lead to no labels being generated for a device that is otherwise trustworthy. To mitigate these scenarios, include a retry option with exponential backoff.

Overview

Sequence diagram that shows the high-level design of the Play Integrity
API

When the user performs a high-value action in your app that you want to protect with an integrity check, complete the following steps:

  1. Your app's server-side backend generates and sends a unique value to the client-side logic. The remaining steps refer to this logic as your "app".
  2. Your app creates the nonce from the unique value and the content of your high-value action. It then calls the Play Integrity API, passing in the nonce.
  3. Your app receives a signed and encrypted verdict from the Play Integrity API.
  4. Your app passes the signed and encrypted verdict to your app's backend.
  5. Your app's backend sends the verdict to a Google Play server. The Google Play server decrypts and verifies the verdict, returning the results to your app's backend.
  6. Your app's backend decides how to proceed, based on the signals contained in the token payload.
  7. Your app's backend sends the decision outcomes to your app.

Generate a nonce

When you protect an action in your app with the Play Integrity API, you can leverage the nonce field to mitigate certain types of attacks, such as person-in-the-middle (PITM) tampering attacks and replay attacks. The Play Integrity API returns the value you set in this field inside the signed integrity response.

The value set in the nonce field must be correctly formatted:

  • String
  • URL-safe
  • Encoded as Base64 and non-wrapping
  • Minimum of 16 characters
  • Maximum of 500 characters

The following are some common ways to use the nonce field in the Play Integrity API. To get the strongest protection from the nonce, you can combine the methods below.

Include a request hash to protect against tampering

You can use the nonce parameter in a classic API request similarly to the requestHash parameter in a standard API request to protect the contents of a request against tampering.

When you request an integrity verdict:

  1. Compute a digest of all critical request parameters (e.g. SHA256 of a stable request serialization) from the user action or server request that is happening.
  2. Use setNonce to set the nonce field to the value of the computed digest.

When you receive an integrity verdict:

  1. Decode and verify the integrity token, and obtain the digest from the nonce field.
  2. Compute a digest of the request in the same manner as in the app (e.g. SHA256 of a stable request serialization).
  3. Compare the app-side and server-side digests. If they do not match, the request is not trustworthy.

Include unique values to protect against replay attacks

In order to prevent malicious users from reusing previous responses from the Play Integrity API, you can use the nonce field to uniquely identify each message.

When you request an integrity verdict:

  1. Obtain a globally unique value in a way that malicious users cannot predict. For example, a cryptographically-secure random number generated on the server side can be such a value, or a pre-existing ID, such as a session or a transaction ID. A simpler and less secure variant is to generate a random number on the device. We recommend creating values 128 bits or larger.
  2. Call setNonce() to set the nonce field to the unique value from step 1.

When you receive an integrity verdict:

  1. Decode and verify the integrity token, and obtain the unique value from the nonce field.
  2. If the value from step 1 was generated on the server, check that the received unique value was one of the generated values, and that it's being used for the first time (your server will need to keep a record of generated values for a suitable duration). If the received unique value has been used already or does not appear in the record, reject the request
  3. Otherwise, if the unique value was generated on the device, check that the received value is being used for the first time (your server needs to keep a record of already seen values for a suitable duration). If the received unique value has been used already, reject the request.

Combine both protections against tampering and replay attacks (recommended)

It is possible to use the nonce field to protect against both tampering and replay attacks at the same time. To do so, generate the unique value as described above, and include it as part of your request. Then compute the request hash, making sure to include the unique value as part of the hash. An implementation that combines both approaches is as follows:

When you request an integrity verdict:

  1. The user initiates the high-value action.
  2. Obtain a unique value for this action as described in the Include unique values to protect against replay attacks section.
  3. Prepare a message you want to protect. Include the unique value from step 2 in the message.
  4. Your app calculates a digest of the message it wants to protect, as described in the Include a request hash to protect against tampering section. Since the message contains the unique value, the unique value is part of the hash.
  5. Use setNonce() to set the nonce field to the computed digest from the previous step.

When you receive an integrity verdict:

  1. Obtain the unique value from the request
  2. Decode and verify the integrity token, and obtain the digest from the nonce field.
  3. As described in the Include a request hash to protect against tampering section, recompute the digest on the server side, and check that it matches the digest obtained from the integrity token.
  4. As described in the Include unique values to protect against replay attacks section, check the validity of the unique value.

The following sequence diagram illustrates these steps with a server-side nonce:

Sequence diagram that shows how to protect against both tampering and replay
attacks

Request an integrity verdict

After generating a nonce, you can request an integrity verdict from Google Play. To do so, complete the following steps:

  1. Create an IntegrityManager, as shown in the following examples.
  2. Construct an IntegrityTokenRequest, supplying the nonce through the setNonce() method in the associated builder. Apps exclusively distributed outside of Google Play and SDKs also have to specify their Google Cloud project number through the setCloudProjectNumber() method. Apps on Google Play are linked to a Cloud project in the Play Console and do not need to set the Cloud project number in the request.
  3. Use the manager to call requestIntegrityToken(), supplying the IntegrityTokenRequest.

Kotlin

// Receive the nonce from the secure server.
val nonce: String = ...

// Create an instance of a manager.
val integrityManager =
    IntegrityManagerFactory.create(applicationContext)

// Request the integrity token by providing a nonce.
val integrityTokenResponse: Task<IntegrityTokenResponse> =
    integrityManager.requestIntegrityToken(
        IntegrityTokenRequest.builder()
             .setNonce(nonce)
             .build())

Java

import com.google.android.gms.tasks.Task; ...

// Receive the nonce from the secure server.
String nonce = ...

// Create an instance of a manager.
IntegrityManager integrityManager =
    IntegrityManagerFactory.create(getApplicationContext());

// Request the integrity token by providing a nonce.
Task<IntegrityTokenResponse> integrityTokenResponse =
    integrityManager
        .requestIntegrityToken(
            IntegrityTokenRequest.builder().setNonce(nonce).build());

Unity

IEnumerator RequestIntegrityTokenCoroutine() {
    // Receive the nonce from the secure server.
    var nonce = ...

    // Create an instance of a manager.
    var integrityManager = new IntegrityManager();

    // Request the integrity token by providing a nonce.
    var tokenRequest = new IntegrityTokenRequest(nonce);
    var requestIntegrityTokenOperation =
        integrityManager.RequestIntegrityToken(tokenRequest);

    // Wait for PlayAsyncOperation to complete.
    yield return requestIntegrityTokenOperation;

    // Check the resulting error code.
    if (requestIntegrityTokenOperation.Error != IntegrityErrorCode.NoError)
    {
        AppendStatusLog("IntegrityAsyncOperation failed with error: " +
                requestIntegrityTokenOperation.Error);
        yield break;
    }

    // Get the response.
    var tokenResponse = requestIntegrityTokenOperation.GetResult();
}

Native

/// Create an IntegrityTokenRequest opaque object.
const char* nonce = RequestNonceFromServer();
IntegrityTokenRequest* request;
IntegrityTokenRequest_create(&request);
IntegrityTokenRequest_setNonce(request, nonce);

/// Prepare an IntegrityTokenResponse opaque type pointer and call
/// IntegerityManager_requestIntegrityToken().
IntegrityTokenResponse* response;
IntegrityErrorCode error_code =
        IntegrityManager_requestIntegrityToken(request, &response);

/// ...
/// Proceed to polling iff error_code == INTEGRITY_NO_ERROR
if (error_code != INTEGRITY_NO_ERROR)
{
    /// Remember to call the *_destroy() functions.
    return;
}
/// ...
/// Use polling to wait for the async operation to complete.
/// Note, the polling shouldn't block the thread where the IntegrityManager
/// is running.

IntegrityResponseStatus response_status;

/// Check for error codes.
IntegrityErrorCode error_code =
        IntegrityTokenResponse_getStatus(response, &response_status);
if (error_code == INTEGRITY_NO_ERROR
    && response_status == INTEGRITY_RESPONSE_COMPLETED)
{
    const char* integrity_token = IntegrityTokenResponse_getToken(response);
    SendTokenToServer(integrity_token);
}
/// ...
/// Remember to free up resources.
IntegrityTokenRequest_destroy(request);
IntegrityTokenResponse_destroy(response);
IntegrityManager_destroy();

Decrypt and verify the integrity verdict

When you request an integrity verdict, the Play Integrity API provides a signed response token. The nonce that you include in your request becomes part of the response token.

Token format

The token is a nested JSON Web Token (JWT), that is JSON Web Encryption (JWE) of JSON Web Signature (JWS). The JWE and JWS components are represented using compact serialization .

The encryption / signing algorithms are well-supported across various JWT implementations:

  • JWE uses A256KW for alg and A256GCM for enc

  • JWS uses ES256.

Decrypt and verify on Google's servers (recommended)

The Play Integrity API allows you to decrypt and verify the integrity verdict on Google's servers, which enhances your app's security. To do so, complete these steps:

  1. Create a service account within the Google Cloud project that's linked to your app.
  2. On your app's server, fetch the access token from your service account credentials using the playintegrity scope, and make the following request:

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
    '{ "integrity_token": "INTEGRITY_TOKEN" }'
  3. Read the JSON response.

Decrypt and verify locally

If you choose to manage and download your response encryption keys, you can decrypt and verify the returned token within your own secure server environment. You can obtain the returned token by using the IntegrityTokenResponse#token() method.

The following example shows how to decode the AES key and the DER-encoded public EC key for signature verification from the Play Console to language-specific (the Java programming language, in our case) keys in the app's backend. Note that the keys are base64-encoded using default flags.

Kotlin

// base64OfEncodedDecryptionKey is provided through Play Console.
var decryptionKeyBytes: ByteArray =
    Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT)

// Deserialized encryption (symmetric) key.
var decryptionKey: SecretKey = SecretKeySpec(
    decryptionKeyBytes,
    /* offset= */ 0,
    AES_KEY_SIZE_BYTES,
    AES_KEY_TYPE
)

// base64OfEncodedVerificationKey is provided through Play Console.
var encodedVerificationKey: ByteArray =
    Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT)

// Deserialized verification (public) key.
var verificationKey: PublicKey = KeyFactory.getInstance(EC_KEY_TYPE)
    .generatePublic(X509EncodedKeySpec(encodedVerificationKey))

Java


// base64OfEncodedDecryptionKey is provided through Play Console.
byte[] decryptionKeyBytes =
    Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT);

// Deserialized encryption (symmetric) key.
SecretKey decryptionKey =
    new SecretKeySpec(
        decryptionKeyBytes,
        /* offset= */ 0,
        AES_KEY_SIZE_BYTES,
        AES_KEY_TYPE);

// base64OfEncodedVerificationKey is provided through Play Console.
byte[] encodedVerificationKey =
    Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT);
// Deserialized verification (public) key.
PublicKey verificationKey =
    KeyFactory.getInstance(EC_KEY_TYPE)
        .generatePublic(new X509EncodedKeySpec(encodedVerificationKey));

Next, use these keys to first decrypt the integrity token (JWE part) and then verify and extract the nested JWS part.

Kotlin

val jwe: JsonWebEncryption =
    JsonWebStructure.fromCompactSerialization(integrityToken) as JsonWebEncryption
jwe.setKey(decryptionKey)

// This also decrypts the JWE token.
val compactJws: String = jwe.getPayload()

val jws: JsonWebSignature =
    JsonWebStructure.fromCompactSerialization(compactJws) as JsonWebSignature
jws.setKey(verificationKey)

// This also verifies the signature.
val payload: String = jws.getPayload()

Java

JsonWebEncryption jwe =
    (JsonWebEncryption)JsonWebStructure
        .fromCompactSerialization(integrityToken);
jwe.setKey(decryptionKey);

// This also decrypts the JWE token.
String compactJws = jwe.getPayload();

JsonWebSignature jws =
    (JsonWebSignature) JsonWebStructure.fromCompactSerialization(compactJws);
jws.setKey(verificationKey);

// This also verifies the signature.
String payload = jws.getPayload();

The resulting payload is a plain-text token that contains integrity verdicts.