Handle BillingResult response codes

When a Play Billing Library call triggers an action, the library returns a BillingResult response to inform developers of the outcome. For example, if you use queryProductDetailsAsync to get the available offers for the user, the response code either contains an OK code and provides the right ProductDetails object, or it contains a different response that indicates the reason why the ProductDetails object couldn’t be provided.

Not all response codes are errors. The BillingResponseCode reference page provides a detailed description of each of the responses discussed in this guide. Some examples of response codes that don't indicate errors are:

When the response code does indicate an error, the cause is sometimes due to transient conditions, and thus recovery is possible. When a call to a Play Billing Library method returns a BillingResponseCode value that indicates a recoverable condition, you should retry the call. In other cases, conditions are not considered transient and therefore a retry is not recommended.

Transient errors call for different retry strategies depending on factors like whether the error happens when users are in session—for example, when a user is going through a purchase flow—or the error happens in the background—for example, when you’re querying the user’s existing purchases during onResume. The retry strategies section below provides examples of these different strategies and the Retriable BillingResult Responses section recommends which strategy works best for each response code.

In addition to the response code, some error responses include messages for debugging and logging purposes.

Retry strategies

Simple retry

In situations where the user is in session, it’s better to implement a simple retry strategy so that the error disrupts the user experience as little as possible. In that case, we recommend a simple retry strategy with a maximum number of attempts as an exit condition.

The following example demonstrates a simple retry strategy to handle an error when establishing a BillingClient connection:

class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {
  // Initialize the BillingClient.
  private val billingClient = BillingClient.newBuilder(context)
    .setListener(this)
    .enablePendingPurchases()
    .build()

  // Establish a connection to Google Play.
  fun startBillingConnection() {
    billingClient.startConnection(object : BillingClientStateListener {
      override fun onBillingSetupFinished(billingResult: BillingResult) {
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
          Log.d(TAG, "Billing response OK")
          // The BillingClient is ready. You can now query Products Purchases.
        } else {
          Log.e(TAG, billingResult.debugMessage)
          retryBillingServiceConnection()
        }
      }

      override fun onBillingServiceDisconnected() {
        Log.e(TAG, "GBPL Service disconnected")
        retryBillingServiceConnection()
      }
    })
  }

  // Billing connection retry logic. This is a simple max retry pattern
  private fun retryBillingServiceConnection() {
    val maxTries = 3
    var tries = 1
    var isConnectionEstablished = false
    do {
      try {
        billingClient.startConnection(object : BillingClientStateListener {
          override fun onBillingSetupFinished(billingResult: BillingResult) {
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
              isConnectionEstablished = true
              Log.d(TAG, "Billing connection retry succeeded.")
            } else {
              Log.e(
                TAG,
                "Billing connection retry failed: ${billingResult.debugMessage}"
              )
            }
          }
        })
      } catch (e: Exception) {
        e.message?.let { Log.e(TAG, it) }
        tries++
      }
    } while (tries <= maxTries && !isConnectionEstablished)
  }
  ...
}

Exponential backoff retry

We recommend using exponential backoff for Play Billing Library operations that happen in the background and don't affect the user experience while the user is in session.

For example, it would be appropriate to implement this when acknowledging new purchases because this operation can happen in the background, and acknowledgment doesn't need to happen in real time if an error occurs.

private fun acknowledge(purchaseToken: String): BillingResult {
  val params = AcknowledgePurchaseParams.newBuilder()
    .setPurchaseToken(purchaseToken)
    .build()
  var ackResult = BillingResult()
  billingClient.acknowledgePurchase(params) { billingResult ->
    ackResult = billingResult
  }
  return ackResult
}

suspend fun acknowledgePurchase(purchaseToken: String) {

  val retryDelayMs = 2000L
  val retryFactor = 2
  val maxTries = 3

  withContext(Dispatchers.IO) {
    acknowledge(purchaseToken)
  }

  AcknowledgePurchaseResponseListener { acknowledgePurchaseResult ->
    val playBillingResponseCode =
    PlayBillingResponseCode(acknowledgePurchaseResult.responseCode)
    when (playBillingResponseCode) {
      BillingClient.BillingResponseCode.OK -> {
        Log.i(TAG, "Acknowledgement was successful")
      }
      BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> {
        // This is possibly related to a stale Play cache.
        // Querying purchases again.
        Log.d(TAG, "Acknowledgement failed with ITEM_NOT_OWNED")
        billingClient.queryPurchasesAsync(
          QueryPurchasesParams.newBuilder()
            .setProductType(BillingClient.ProductType.SUBS)
            .build()
        )
        { billingResult, purchaseList ->
          when (billingResult.responseCode) {
            BillingClient.BillingResponseCode.OK -> {
              purchaseList.forEach { purchase ->
                acknowledge(purchase.purchaseToken)
              }
            }
          }
        }
      }
      in setOf(
         BillingClient.BillingResponseCode.ERROR,
         BillingClient.BillingResponseCode.SERVICE_DISCONNECTED,
         BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
       ) -> {
        Log.d(
          TAG,
          "Acknowledgement failed, but can be retried --
          Response Code: ${acknowledgePurchaseResult.responseCode} --
          Debug Message: ${acknowledgePurchaseResult.debugMessage}"
        )
        runBlocking {
          exponentialRetry(
            maxTries = maxTries,
            initialDelay = retryDelayMs,
            retryFactor = retryFactor
          ) { acknowledge(purchaseToken) }
        }
      }
      in setOf(
         BillingClient.BillingResponseCode.BILLING_UNAVAILABLE,
         BillingClient.BillingResponseCode.DEVELOPER_ERROR,
         BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED,
       ) -> {
        Log.e(
          TAG,
          "Acknowledgement failed and cannot be retried --
          Response Code: ${acknowledgePurchaseResult.responseCode} --
          Debug Message: ${acknowledgePurchaseResult.debugMessage}"
        )
        throw Exception("Failed to acknowledge the purchase!")
      }
    }
  }
}

private suspend fun <T> exponentialRetry(
  maxTries: Int = Int.MAX_VALUE,
  initialDelay: Long = Long.MAX_VALUE,
  retryFactor: Int = Int.MAX_VALUE,
  block: suspend () -> T
): T? {
  var currentDelay = initialDelay
  var retryAttempt = 1
  do {
    runCatching {
      delay(currentDelay)
      block()
    }
      .onSuccess {
        Log.d(TAG, "Retry succeeded")
        return@onSuccess;
      }
      .onFailure { throwable ->
        Log.e(
          TAG,
          "Retry Failed -- Cause: ${throwable.cause} -- Message: ${throwable.message}"
        )
      }
    currentDelay *= retryFactor
    retryAttempt++
  } while (retryAttempt < maxTries)

  return block() // last attempt
}

Retriable BillingResult responses

NETWORK_ERROR (Error Code 12)

Problem

This error indicates that there was a problem with the network connection between the device and Play systems.

Possible resolution

To recover, use simple retries or exponential backoff, depending on which action triggered the error.

SERVICE_TIMEOUT (Error Code -3)

Problem

This error indicates that the request has reached the maximum timeout before Google Play is able to respond. This could be caused, for example, by a delay in the execution of the action requested by the Play Billing Library call.

Possible resolution

This is usually a transient issue. Retry the request using either either a simple or exponential backoff strategy, depending on which action returned the error.

Unlike SERVICE_DISCONNECTED below, the connection to the Google Play Billing service is not severed, and you only need to retry whatever Play Billing Library operation was attempted.

SERVICE_DISCONNECTED (Error Code -1)

Problem

This fatal error indicates that the client app’s connection to the Google Play Store service via the BillingClient has been severed.

Possible resolution

To avoid this error as much as possible, always check the connection to Google Play services before making calls with the Play Billing Library by calling BillingClient.isReady().

To attempt recovery from SERVICE_DISCONNECTED , your client app should try to re-establish the connection using BillingClient.startConnection.

Just like with SERVICE_TIMEOUT , use simple retries or exponential backoff, depending on which action triggered the error.

SERVICE_UNAVAILABLE (Error Code 2)

Important Note:

Starting in Google Play Billing Library 6.0.0, SERVICE_UNAVAILABLE is no longer returned for network issues. It is returned when the billing service is unavailable and the deprecated SERVICE_TIMEOUT case scenarios.

Problem

This transient error indicates the Google Play Billing service is currently unavailable. In most cases, this means there is a network connection issue anywhere between the client device and Google Play Billing services.

Possible resolution

This is usually a transient issue. Retry the request using either either a simple or exponential backoff strategy, depending on which action returned the error.

Unlike SERVICE_DISCONNECTED , the connection to the Google Play Billing service is not severed, and you need to retry whatever operation is being attempted.

BILLING_UNAVAILABLE (Error Code 3)

Problem

This error indicates that a user billing error occurred during the purchase process. Examples of when this can occur include:

  • The Play Store app on the user's device is out of date.
  • The user is in an unsupported country.
  • The user is an enterprise user, and their enterprise admin has disabled users from making purchases.
  • Google Play is unable to charge the user’s payment method. For example, the user's credit card might have expired.

Possible resolution

Automatic retries are unlikely to help in this case. However, a manual retry can help if the user addresses the condition that caused the issue. For example, if the user updates their Play Store version to a supported version, then a manual retry of the initial operation could work.

If this error occurs when the user is not in session, retrying might not make sense. When you receive a BILLING_UNAVAILABLE error as a result of the purchase flow, it’s very likely the user received feedback from Google Play during the purchase process and might be aware of what went wrong. In this case, you could show an error message specifying something went wrong and offer a “Try again” button to give the user the option of a manual retry after they address the issue.

ERROR (Error Code 6)

Problem

This is a fatal error that indicates an internal problem with Google Play itself.

Possible resolution

Sometimes internal Google Play problems that lead to ERROR are transient, and a retry with an exponential backoff can be implemented for mitigation. When users are in session, a simple retry is preferable.

ITEM_ALREADY_OWNED

Problem

This response indicates that the Google Play user already owns the subscription or one-time purchase product they are attempting to purchase. In most cases, this is not a transient error, except when it is caused by a stale Google Play’s cache.

Possible resolution

To avoid this error happening when the cause is not a cache issue, don't offer a product for purchase when the user already owns it. Make sure you check the user’s entitlements when you show the products available for purchase, and filter what the user can purchase accordingly. When the client app receives this error due to a cache issue, the error triggers Google Play’s cache to get updated with the latest data from Play’s backend. Retrying after the error should resolve this specific transient instance in this case. Call BillingClient.queryPurchasesAsync() after getting an ITEM_ALREADY_OWNED to check if the user has acquired the product, and if it’s not the case implement a simple retry logic to reattempt the purchase.

ITEM_NOT_OWNED

Problem

This purchase response indicates that the Google Play user does not own the subscription or one-time purchase product the user is attempting to replace, acknowledge or consume. This is not a transient error in most cases except when it is caused by Google Play’s cache getting into a stale state.

Possible resolution

When the error is received because of a cache issue, the error triggers Google Play’s cache to get updated with the latest data from Play’s backend. Retrying with a simple retry strategy after the error should resolve this specific transient instance. Call BillingClient.queryPurchasesAsync() after getting an ITEM_NOT_OWNED to check if the user has acquired the product. If they have not, use simple retry logic to reattempt the purchase.

Non-Retriable BillingResult responses

You can't recover from these errors using retry logic.

FEATURE_NOT_SUPPORTED

Problem

This non-retriable error indicates that the Google Play Billing feature is not supported on the user's device, likely due to an old Play Store version.

For example, perhaps some of your users' devices don't support in-app messaging.

Possible mitigation

Use BillingClient.isFeatureSupported() to check feature support before making the call to the Play Billing Library.

when {
  billingClient.isReady -> {
    if (billingClient.isFeatureSupported(BillingClient.FeatureType.IN_APP_MESSAGING)) {
       // use feature
    }
  }
}

USER_CANCELED

Problem

The user has clicked out of the billing flow UI.

Possible resolution

This is informational only and can fail gracefully.

ITEM_UNAVAILABLE

Problem

The Google Play Billing subscription or one-time purchase product is not available for purchase for this user.

Possible mitigation

Make sure your app refreshes the product details via queryProductDetailsAsync as recommended. Take into account how often your product catalog changes on the Play Console configuration to implement extra refreshes if needed. Only attempt to sell products on Google Play Billing that return the right information via queryProductDetailsAsync. Check the product eligibility configuration for any inconsistencies. For example, you might be querying for a product that is only available for a region other than the one the user is trying to purchase. To be available for purchase, a product needs to be active, its app needs to be published, and its app needs to be available in the user's country.

Sometimes, in particular during testing, everything is correct in the product configuration, but users still see this error. This might be due to a propagation delay of the product details across Google’s servers. Try again later.

DEVELOPER_ERROR

Problem

This is a fatal error that indicates you're improperly using an API. For example, supplying incorrect parameters to BillingClient.launchBillingFlow can cause this error.

Possible resolution

Make sure that you are correctly using the different Play Billing Library calls. Also, check the debug message for more info about the error.