Develop
Develop
Select your platform

Implement the Google Play Billing Interface

Updated: May 19, 2025
This page explains how your app can implement in-app purchasing using the Meta Horizon Billing Compatibility SDK. If you’ve already integrated your app with Google Play Billing Library, you’ll make minimal or no code changes in most steps.
For a detailed API reference, see the SDK’s API reference.

Before you begin

Remove unsupported fields

The following fields that are available in Google Play Billing are not supported in the Meta Horizon Billing Compatibility SDK. Remove references to these fields from your code.
Request fields:
  • Account identifiers (Obfuscated Account ID and Obfuscated Profile ID)
Response fields:
  • Account identifiers (Obfuscated Account ID and Obfuscated Profile ID)
  • Order ID
  • Signature
  • Acknowledged

Initialize a BillingClient

Initialize a BillingClient instance. A BillingClient object enables communication between Meta Horizon Billing Compatibility APIs and your app. The BillingClient provides asynchronous convenience methods for many common billing operations.
Like Google Play Billing, it’s strongly recommended that you instantiate only one BillingClient instance at a time. However, with the Meta Horizon Billing Compatibility SDK, instantiating multiple BillingClient instances at a time doesn’t result in multiple PurchasesUpdatedListener callbacks for a single purchase event. Instead, calls to a specific instantiation of BillingClient update the associated PurchasesUpdatedListener. Create a BillingClient using the newBuilder() method. Ensure you pass in an Activity as the context. To receive updates on purchases, add a listener by calling setListener(). Pass a PurchasesUpdatedListener object to the setListener() method.
The enablePendingPurchases() method is only available as a no-op method . It doesn’t enable pending purchases, but remains here for compatibility as Google’s library requires its usage.
The following code shows how to initialize a BillingClient.
  private PurchasesUpdatedListener purchasesUpdatedListener =
      new PurchasesUpdatedListener() {
        @Override
        public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
          // TODO: implement
        }
      };
  private BillingClient billingClient =
      BillingClint.newBuilder(activity)
          .setListener(purchasesUpdatedListener)
          .setAppId(APP_ID)
          .enablePendingPurchases()
          .build();

Connect to the Meta Horizon Store

You don’t need to make code changes here, but the details of what’s happening will be explained.
On calling startConnection(), the BillingClientStateListener typically receives a callback with BillingResponseCode.OK. The onBillingServiceDisconnected() method is provided as a no-op method, which is never invoked by the Meta Horizon Billing Compatibility SDK.
The following example demonstrates how to connect to the Meta Horizon Store.
    billingClient.startConnection(
        new BillingClientStateListener() {
          @Override
          public void onBillingSetupFinished(BillingResult billingResult) {
            if (billingResult.getResponseCode() == BillingResponseCode.OK) {
              // Query products and purchases here.
            } else {
              // The underlying connection may fail...
            }
          }

          @Override
          public void onBillingServiceDisconnected() {
            // This is a no-op method, which is never invoked by the Meta Horizon Billing
            // Compatibility SDK.
          }
        });

Show products available to buy

You don’t need to make code changes here.
Before showing products to your users, make sure to query for product details to get localized product information. You can call queryProductDetailsAsync() to query for in-app product details.
The only error response codes the Meta Horizon Billing Compatibility SDK returns are BillingResponseCode.SERVICE_DISCONNECTED, and BillingResponseCode.DEVELOPER_ERROR and BillingResponseCode.ERROR. Other error response codes supported by Google Play Billing are available, but are never returned.

QueryProductDetailsAsync API

You can query for product details with the queryProductDetailsAsync() method. This method takes an instance of QueryProductDetailsParams. The QueryProductDetailsParams object specifies a list of product ID strings you created in the Meta Horizon Developer Center, along with a ProductType. For consumables and durables, the ProductType is ProductType.INAPP. For subscriptions, the ProductType is ProductType.SUBS.
For subscription products, the API returns a list of subscription offer details, List<ProductDetails.SubscriptionOfferDetails>, that contains all offers available to the user. Each offer has a unique offer token, which you can access by using the getOfferToken() method. You must pass the offer token when launching the purchase flow. You can access the offer details of a one-time purchase in-app item with the getOneTimePurchaseOfferDetails() method of the API response.
To handle the result of the asynchronous operation, the queryProductDetailsAsync() method also requires a listener. This listener is your implementation of the ProductDetailsResponseListener interface, where you override onProductDetailsResponse(). The onProductDetailsResponse() method notifies the listener when the product details query finishes, as shown in the following example.
    QueryProductDetailsParams queryProductDetailsParams =
        QueryProductDetailsParams.newBuilder()
            .setProductList(
                ImmutableList.of(
                    Product.newBuilder()
                        .setProductId("product_id_example")
                        .setProductType(ProductType.INAPP)
                        .build()))
            .build();
    billingClient.queryProductDetailsAsync(
        queryProductDetailsParams,
        new ProductDetailsResponseListener() {
          public void onProductDetailsResponse(
              BillingResult billingResult, List<ProductDetails> productDetailsList) {
            if (billingResult.getResponseCode() == BillingResponseCode.OK) {
              // Process the returned ProductDetails List
            } else if (billingResult.getResponseCode() == BillingResponseCode.ERROR) {
              // Handle the error response.
            } else if (billingResult.getResponseCode() == BillingResponseCode.DEVELOPER_ERROR) {
              // Handle the developer error response.
              // Typically this is a sign the developer input was incorrect/in a bad format
            } else if (billingResult.getResponseCode()
                == BillingResponseCode.SERVICE_DISCONNECTED) {
              // startConnection() wasn't called on the BillingClient
            } else {
              // Other error codes are available, but are never returned by the
              // Billing Compatibility SDK.
            }
          }
        });
Unlike Google Play Billing, ProductDetails.getTitle() does not include the app name.

Launch the purchase flow

You might need to make minimal code changes here.
Unlike Google Play Billing, the Meta Horizon Billing Compatibility SDK allows at most one product in a single purchase. If the list has more than one item, the error BillingResponseCode.FEATURE_NOT_SUPPORTED is returned.
For your app to start the purchase flow, call launchBillingFlow() from your app’s main thread. The launchBillingFlow() method takes a BillingFlowParams object, which contains a ProductDetails object. You can get ProductDetails by calling queryProductDetailsAsync(). Create a BillingFlowParams object by using the BillingFlowParams.Builder class. The following example shows how to launch the billing flow.
    // An activity reference from which the billing flow is launched
    Activity activity = ...;
    List productDetailsParamsList =
        List.of(
            ProductDetailsParams.newBuilder()
                // Call queryProductDetailsAsync to get productDetails
                .setProductDetails(productDetails)
                .build());
    BillingFlowParams billingFlowParams =
        BillingFlowParams.newBuilder()
            .setProductDetailsParamsList(productDetailsParamsList)
            .build();
    // Launch the billing flow
    BillingResult billingResult = billingClient.launchBillingFlow(activity, billingFlowParams);
The following code shows an example of setting an offer token for a purchase. For more details about the offer token, see the QueryProductDetailsAsync API.
    BillingFlowParams billingFlowParams =
        BillingFlowParams.newBuilder()
            .setProductDetailsParamsList(productDetailsParamsList)
            .setOfferToken(offerDetails.getOfferToken())
            .build();
When you initialized your BillingClient, you used setLister() to add your implementation of PurchasesUpdatedListener as a listener. This listener overrides the onPurchasesUpdated() method, which delivers the result of your purchase. Your implementation of onPurchasesUpdated() must handle the possible response codes, as shown in the following example.
  @Override
  void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
    if (billingResult.getResponseCode() == BillingResponseCode.OK) {
      for (Purchase purchase : purchases) {
        handlePurchase(purchase);
      }
    } else if (billingResult.getResponseCode() == BillingResponseCode.ERROR) {
      // usually denotes an error with the request/backend
    } else if (billingResult.getResponseCode() == BillingResponseCode.DEVELOPER_ERROR) {
      // typically this involves incorrect inputs
    } else if (billingResult.getResponseCode() == BillingResponseCode.SERVICE_DISCONNECTED) {
      // the developer did not call startConnection() on BillingClient first
    } else {
      // Other error codes are available, but typically are not returned
    }
  }
The only error response codes the Meta Horizon Billing Compatibility SDK returns are BillingResponseCode.SERVICE_DISCONNECTED, and BillingResponseCode.DEVELOPER_ERROR and BillingResponseCode.ERROR. Other error response codes supported by Google Play Billing are available, but are never returned.
When a purchase is successful, a purchase token is generated. A purchase token uniquely identifies a purchase and represents the user and the product ID associated with the purchase.

Process purchases

You might need to make minimal code changes here.
After a user completes a purchase, your app needs to process that purchase. Your app is usually notified of purchases through your PurchasesUpdatedListener. However, there are cases where your app uses queryPurchasesAsync() to fetch purchases, as described in Fetch Purchases.
On completing a purchase, your app should give the content to the user. For entitlements, acknowledge delivery of the content using acknowledgePurchase(). For consumables, call consumeAsync() to acknowledge delivery and mark the item as consumed.
Review these differences between the Meta Horizon Billing API interface and the Google Play Billing Library:
  • Calling acknowledgePurchase() is a no-op. We have no specific acknowledgement requirements for subscription or durable purchases.
  • Developers should only call consumeAsync() on consumable items.
  • On non-acknowledgement of purchases, users do not automatically receive refunds. This is different from Google Play Billing, where purchases are revoked on non-acknowledgement for three days as detailed in the Android developer documentation.
The following example shows how to consume a product using the associated purchase token:
  void handlePurchase(Purchase purchase) {
    // Purchase retrieved from queryPurchasesAsync or your PurchasesUpdatedListener.
    if (purchase.getPurchaseState() != PurchaseState.PURCHASED) {
      return;
    }
    // Deliver item to user.
    // Consume item.
    ConsumeParams consumeParams =
        ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build();
    ConsumeResponseListener listener =
        new ConsumeResponseListener() {
          @Override
          public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
            if (billingResult.getResponseCode() == BillingResponseCode.OK) {
              // Handle the success of the consume operation.
            } else if (billingResult.getResponseCode() == BillingResponseCode.ERROR) {
              // Handle the error response.
            } else {
              // handle misc errors...
            }
          }
        };
    billingClient.consumeAsync(consumeParams, listener);
  }

Fetch purchases

You might need to make minimal code changes here.
Although your app is notified of purchases when listening through PurchasesUpdatedListener, certain scenarios might cause your app to be unaware of a purchase a user has made. Scenarios where your app could be unaware of purchases are:
  • Network issues: A user makes a successful purchase, but their device has a network connection failure before being notified of the purchase through PurchasesUpdatedListener.
  • Multiple devices: A user buys an item on a device, switches to another device, and expects to see the item they purchased.
  • Subscription lifecycle events: Subscription lifecycle events like renewals occur periodically without API calls from the billing client.
You can handle these scenarios by calling queryPurchasesAsync() in your onResume() method. This ensures all purchases are successfully processed, as described in Process purchases. Unlike Google Play Billing, queryPurchasesAsync() makes a network call when the local cache expires, which affects the time it takes for the listener callback to occur. To reduce the call times in the Meta Horizon Billing Compatibility SDK, limit the number of SKUs to 100 in a queryPurchasesAsync() call.
The queryPurchasesAsync() method returns only purchases for durable and non-consumed consumables and active subscriptions. The following example shows how to fetch a user’s in-app purchases:
billingClient.queryPurchasesAsync(
    QueryPurchasesParams.newBuilder().setProductType(ProductType.INAPP).build(),
    new PurchasesResponseListener() {
        public void onQueryPurchasesResponse(
            BillingResult billingResult, List<Purchase> purchases) {
        if (billingResult.getResponseCode() == BillingResponseCode.OK) {
            // handle the return purchase listing
        } else if (billingResult.getResponseCode() == BillingResponseCode.ERROR) {
            // Handle the error response from the backend/network
        } else if (billingResult.getResponseCode() == BillingResponseCode.DEVELOPER_ERROR) {
            // typically involves incorrect inputs
        } else {
            // other error types are available, but not typically returned
        }
        }
    });
For subscriptions, pass ProductType.SUBS, while creating QueryPurchasesParams as shown here.
QueryPurchasesParams.newBuilder().setProductType(ProductType.SUBS).build()
The only error response codes the Appstore Billing Compatibility SDK returns are BillingResponseCode.SERVICE_DISCONNECTED, and BillingResponseCode.DEVELOPER_ERROR and BillingResponseCode.ERROR. Other error response codes supported by Google Play Billing are available, but are never returned.
Did you find this page helpful?