How to Implement In-App Purchases in the Unity Engine (2026)

Unity’s In‑App Purchasing (IAP) has gone through a significant evolution with the release of IAP Version 5. In this blog, we walk you through implementing in-app purchases using Unity’s latest IAP v5. For reference, we will be using the Unity IAP version 5.1.2 and the Unity Editor version 6000.3.8f1, which are the most current versions at the time of writing.

Install the Unity IAP Package

If you haven’t already installed the IAP package, open the Package Manager by navigating to Window -> Package Management -> Package Manager.

Package manager.

When the Package Manager window opens, click Unity Registry on the left side and wait for it to refresh the packages list. Scroll to the In-App Purchasing package, or search it in the search bar. Then select it, click Install and wait for it to finish installing.

IAP installation window.

Implement the Unity IAP Code

Create a new C# script by right-clicking the Assets window at the bottom and select Create -> Scripting -> MonoBehaviour Script. Name it IAPManager.

Adding script.
IAPManager file.

Double-click the IAPManager.cs script to open it in Visual Studio, or you can use your favorite text editor. Import the Unity.Services.Core and UnityEngine.Purchasing modules.


using UnityEngine;
using Unity.Services.Core;
using UnityEngine.Purchasing;

public class IAPManager : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Replace the Start() function to initialize the UnityServices and IAP.


using UnityEngine;
using Unity.Services.Core;
using UnityEngine.Purchasing;
using System;

public class IAPManager : MonoBehaviour
{
    private static bool isStoreInitialized = false;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    async void Start()
    {
        try
        {
            if (!isStoreInitialized)
            {
                await UnityServices.InitializeAsync();
            }
        }
        catch (Exception)
        {
            Debug.Log("Failed to initialize Unity Services.");
        }
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

The isStoreInitialized flag is set to true once Unity IAP finishes its initialization process. Since this only needs to happen once during the game’s lifetime, the Start() method checks whether the flag is still false. If it is, the store hasn’t been initialized yet, so we trigger the setup. And if you’re wondering why the flag isn’t set to true here, don’t worry, we update it later in the initialization callback once the system confirms everything is ready.

Note that the Start() function is preceded with the keyword async, allowing it to run asynchronously. This prevents the UI thread from being blocked during IAP initialization, avoiding freezes or stutters that would degrade the user experience. The await keyword means the code will wait until the UnityServices.InitializeAsync() is finished, but again, won’t block the main thread.

Now let’s initialize the IAP.


using UnityEngine;
using Unity.Services.Core;
using UnityEngine.Purchasing;
using System;
using System.Collections.Generic;

public class IAPManager : MonoBehaviour
{
    private static bool isStoreInitialized = false;
    private static StoreController storeController;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    async void Start()
    {
        try
        {
            if (!isStoreInitialized)
            {
                await UnityServices.InitializeAsync();
                InitializePurchasing();
            }
        }
        catch (Exception)
        {
            Debug.Log("Failed to initialize Unity Services.");
        }
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private async void InitializePurchasing()
    {
        if (isStoreInitialized)
        {
            return;
        }

        storeController = UnityIAPServices.StoreController();

        storeController.OnPurchasesFetched += OnPurchasesFetched;
        storeController.OnPurchasesFetchFailed += OnPurchasesFetchFailed;
        storeController.OnPurchasePending += OnPurchasePending;
        storeController.OnPurchaseFailed += OnPurchaseFailed;
        storeController.OnProductsFetched += OnProductsFetched;
        storeController.OnProductsFetchFailed += OnProductsFetchFailed;
        storeController.OnStoreDisconnected += OnStoreDisconnected;

        await storeController.Connect();

        isStoreInitialized = true;

        var initialProductsToFetch = new List
        {
            new("sampleid", ProductType.NonConsumable),
            new("sampleid2", ProductType.NonConsumable),
            new("sampleid3", ProductType.NonConsumable),
        };

        storeController.FetchProducts(initialProductsToFetch);
    }

    private void OnPurchasesFetched(Orders orders)
    {
    }

    private void OnPurchasesFetchFailed(PurchasesFetchFailureDescription error)
    {
    }

    private void OnPurchasePending(PendingOrder order)
    {
    }

    private void OnPurchaseFailed(FailedOrder order)
    {
    }

    private void OnProductsFetched(List products)
    {
    }

    private void OnProductsFetchFailed(ProductFetchFailed error)
    {
    }

    private void OnStoreDisconnected(StoreConnectionFailureDescription failureDescription)
    {
    }
}

The Start() function calls InitializePurchasing() after the UnityServices has finished initializing.

Inside InitializePurchasing(), we first check whether the store has already been initialized. If it has, we simply return, since the initialization process only needs to run once.

Next, we initialize the store controller using storeController = UnityIAPServices.StoreController(). The StoreController is responsible for managing the entire IAP system. Once it’s set, we subscribe to the callbacks that the IAP service may trigger.


storeController = UnityIAPServices.StoreController();

storeController.OnPurchasesFetched += OnPurchasesFetched;
storeController.OnPurchasesFetchFailed += OnPurchasesFetchFailed;
storeController.OnPurchasePending += OnPurchasePending;
storeController.OnPurchaseFailed += OnPurchaseFailed;
storeController.OnProductsFetched += OnProductsFetched;
storeController.OnProductsFetchFailed += OnProductsFetchFailed;
storeController.OnStoreDisconnected += OnStoreDisconnected;

Here’s what each of these callbacks is responsible for:

  • OnProductsFetched: Triggered when product metadata is successfully retrieved from the Google Play or Apple App Store.
  • OnProductsFetchFailed: Triggered when the system is unable to load product information.
  • OnPurchasesFetched: Triggered when previously purchased items are successfully loaded from the store.
  • OnPurchasesFetchFailed: Triggered when the system cannot load previously purchased items.
  • OnPurchaseFailed: Triggered when a purchase attempt cannot be completed, such as when payment processing fails.
  • OnPurchasePending: Triggered when a purchase has been initiated but is not yet finalized. For example, if a purchase is made but the payment provider takes time to confirm it, the purchase remains pending until validation is complete.
  • OnStoreDisconnected: Triggered when the app loses its connection to the store service.

At this point, the functions exist only to allow the project to compile as they’re still empty. We’ll implement them shortly.

Once that’s done, we attempt to connect to the store by calling await storeController.Connect(). Because this call is asynchronous, the await keyword ensures the method pauses until the connection process completes without blocking the main thread. If the call succeeds, we can safely assume the store is connected and set isStoreInitialized = true. If the connection fails, the OnStoreDisconnected callback will be invoked.

To complete the initialization process, we fetch the IAP products. These products are pulled from your store account, depending on the platform you’re targeting. For Android, this means configuring your IAP products in the Google Play Console. We won’t go into full detail here, but at a high level, adding an IAP product on Android involves the following steps:

  1. Login to your Google Play Console account.
  2. Select your app.
  3. On the left menu, select Monetize with Play.
  4. Click and expand the Products dropdown.
  5. Select One-time products.
  6. Select Create one-time product.
  7. Follow the instructions to create your IAP product.

The overall workflow should be similar for Apple apps. Let’s look at the code snippet where the product definitions are defined.


var initialProductsToFetch = new List
{
    new("sampleid1", ProductType.NonConsumable),
    new("sampleid2", ProductType.NonConsumable),
    new("sampleid3", ProductType.NonConsumable),
};

storeController.FetchProducts(initialProductsToFetch);

The code creates a list of ProductDefinition objects and initializes them using your product IDs. Replace sampleid* instances with the actual product IDs from your store dashboard. In this sample code we’re adding a ProductType.NonConsumable, meaning the item can be purchased only once and remains permanently owned by the user, like a skin pack for instance. If your item is consumable, such as in‑game currency, use ProductType.Consumable. For subscription-based products, use ProductType.Subscription instead.

Next, the code calls storeController.FetchProducts(initialProductsToFetch) to retrieve your products from the store. This requires a working internet connection. In some cases, product data may be cached on the device, allowing the fetch to succeed offline, but it will typically fail without internet access. If the fetch succeeds, the OnProductsFetched callback is invoked; otherwise OnProductsFetchFailed is triggered.

Now let’s generate the GooglePlayTangle and AppleTangle files which will be used for receipt validation. Receipt validation is essential when restoring purchases, as it ensures the items being recovered are legitimate and haven’t been spoofed or tampered with by a malicious user.

Return to the Unity Editor and navigate to Services -> In-App Purchasing -> Configure…

IAP configure window.

Select the In-App Purchasing settings.

IAP project settings menu.

Under Receipt Obfuscator, enter the key from your Google Play Console. You can find this key by following these steps:

  1. Login to your Google Play Console account.
  2. Choose you app.
  3. On the left side menu, select Monetize with Play.
  4. Select Monetization setup.
  5. Under the Licensing section, you will find your Base64-encoded public key.
  6. Copy this key and paste it in the key section in the Unity Editor.
  7. Click the Obfuscate License Keys
Obfuscation settings.

Your GooglePlayTangle.cs file should be now created in your Assets folder in the Scripts -> UnityPurchasing -> generated folder.

Repeat the same process for the Apple tangle file.

Info

This approach is known as client-side receipt validation, because all verification happens directly on the device. While convenient, it’s also easier for attackers to spoof or bypass. A more secure option is server-side validation, where receipts are verified on a server you control, well outside the user’s reach. If you have access to a server, server-side validation is strongly recommended.

Let’s implement our callbacks.


using UnityEngine;
using Unity.Services.Core;
using UnityEngine.Purchasing;
using System;
using System.Collections.Generic;
using UnityEngine.Purchasing.Security;

public class IAPManager : MonoBehaviour
{
    private static bool isStoreInitialized = false;
    private static StoreController storeController;
    private static HashSet<string> playerPurchases = new HashSet<string>();

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    async void Start()
    {
        try
        {
            if (!isStoreInitialized)
            {
                await UnityServices.InitializeAsync();
                InitializePurchasing();
            }
        }
        catch (Exception)
        {
            Debug.Log("Failed to initialize Unity Services.");
        }
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private async void InitializePurchasing()
    {
        if (isStoreInitialized)
        {
            return;
        }

        storeController = UnityIAPServices.StoreController();

        storeController.OnPurchasesFetched += OnPurchasesFetched;
        storeController.OnPurchasesFetchFailed += OnPurchasesFetchFailed;
        storeController.OnPurchasePending += OnPurchasePending;
        storeController.OnPurchaseFailed += OnPurchaseFailed;
        storeController.OnProductsFetched += OnProductsFetched;
        storeController.OnProductsFetchFailed += OnProductsFetchFailed;
        storeController.OnStoreDisconnected += OnStoreDisconnected;

        await storeController.Connect();

        isStoreInitialized = true;

        var initialProductsToFetch = new List
        {
            new("sampleid", ProductType.NonConsumable),
            new("sampleid2", ProductType.NonConsumable),
            new("sampleid3", ProductType.NonConsumable),
        };

        storeController.FetchProducts(initialProductsToFetch);
    }

    private void OnPurchasesFetched(Orders orders)
    {
        foreach (var confirmedOrder in orders.ConfirmedOrders)
        {
            ProcessPurchase(confirmedOrder);
        }
    }

    private void OnPurchasesFetchFailed(PurchasesFetchFailureDescription error)
    {
        Debug.Log($"Failed to fetch purchases - {error.FailureReason}");
    }

    private void OnPurchasePending(PendingOrder order)
    {
        bool result = ProcessPurchase(order);
        if (result)
        {
            storeController.ConfirmPurchase(order);
        }
    }

    private void OnPurchaseFailed(FailedOrder order)
    {
        string productId = order.Info.PurchasedProductInfo.Count > 0 ? order.Info.PurchasedProductInfo[0].productId : null;
        Debug.Log($"Purchase failed for product {productId}");
    }

    private void OnProductsFetched(List products)
    {
        storeController.FetchPurchases();
    }

    private void OnProductsFetchFailed(ProductFetchFailed error)
    {
        Debug.Log($"Failed to fetch products - {error.FailureReason}");
    }

    private void OnStoreDisconnected(StoreConnectionFailureDescription failureDescription)
    {
        Debug.Log($"Store disconnected - {failureDescription.message}");
    }

    private bool ProcessPurchase(Order order)
    {
        string productId = order.Info.PurchasedProductInfo.Count > 0 ? order.Info.PurchasedProductInfo[0].productId : null;
        bool validReceipt = productId != null;

        try
            {
                var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);

                var result = validator.Validate(order.Info.Receipt);

            }
        catch (IAPSecurityException)
        {
            validReceipt = false;
        }

        if (validReceipt)
        {
            playerPurchases.Add(productId);
        }

        return validReceipt;
    }
}


We start by adding a map called playerPurchases, which stores all valid purchases the user has made. This lets us avoid repeatedly fetching purchase data from the store and improves performance.

ProcessPurchase() is responsible for restoring previous purchases. It also validates the purchase’s receipt using the CrossPlatformValidator object.

Next, let’s look at the callbacks. OnProductsFetched triggers a call to FetchPurchases and checks which purchases the user has already made.

OnPurchasesFetched calls ProcessPurchase for each purchase the user has made, ensuring that every restored item is properly validated.

OnPurchasePending verifies that the pending purchase, if any, is valid and then confirms it with the store using the ConfirmPurchase() function. This confirmation step is required to finalize any pending transaction.

OnPurchasesFetchFailedOnProductsFetchFailedOnPurchaseFailed and OnStoreDisconnected simply log messages to Logcat in this example. We haven’t added any UI handling to keep the tutorial straightforward. In a real application, you should provide appropriate user feedback in these callbacks, such as displaying a popup dialog to inform the player when an error occurs.

The final piece of code we need is the part that actually initiates the purchase.


using UnityEngine;
using Unity.Services.Core;
using UnityEngine.Purchasing;
using System;
using System.Collections.Generic;
using UnityEngine.Purchasing.Security;

public class IAPManager : MonoBehaviour
{
    private static bool isStoreInitialized = false;
    private static StoreController storeController;
    private static HashSet<string> playerPurchases = new HashSet<string>();

    public static void BuyProduct(string productId)
    {
        if (isStoreInitialized)
        {
            try
            {
                storeController.PurchaseProduct(productId);
            }
            catch (Exception)
            {
                Debug.Log($"Failed to buy product. {productId}");
            }
        }
    }

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    async void Start()
    {
        try
        {
            if (!isStoreInitialized)
            {
                await UnityServices.InitializeAsync();
                InitializePurchasing();
            }
        }
        catch (Exception)
        {
            Debug.Log("Failed to initialize Unity Services.");
        }
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private async void InitializePurchasing()
    {
        if (isStoreInitialized)
        {
            return;
        }

        storeController = UnityIAPServices.StoreController();

        storeController.OnPurchasesFetched += OnPurchasesFetched;
        storeController.OnPurchasesFetchFailed += OnPurchasesFetchFailed;
        storeController.OnPurchasePending += OnPurchasePending;
        storeController.OnPurchaseFailed += OnPurchaseFailed;
        storeController.OnProductsFetched += OnProductsFetched;
        storeController.OnProductsFetchFailed += OnProductsFetchFailed;
        storeController.OnStoreDisconnected += OnStoreDisconnected;

        await storeController.Connect();

        isStoreInitialized = true;

        var initialProductsToFetch = new List
        {
            new("sampleid", ProductType.NonConsumable),
            new("sampleid2", ProductType.NonConsumable),
            new("sampleid3", ProductType.NonConsumable),
        };

        storeController.FetchProducts(initialProductsToFetch);
    }

    private void OnPurchasesFetched(Orders orders)
    {
        foreach (var confirmedOrder in orders.ConfirmedOrders)
        {
            ProcessPurchase(confirmedOrder);
        }
    }

    private void OnPurchasesFetchFailed(PurchasesFetchFailureDescription error)
    {
        Debug.Log($"Failed to fetch purchases - {error.FailureReason}");
    }

    private void OnPurchasePending(PendingOrder order)
    {
        bool result = ProcessPurchase(order);
        if (result)
        {
            storeController.ConfirmPurchase(order);
        }
    }

    private void OnPurchaseFailed(FailedOrder order)
    {
        string productId = order.Info.PurchasedProductInfo.Count > 0 ? order.Info.PurchasedProductInfo[0].productId : null;
        Debug.Log($"Purchase failed for product {productId}");
    }

    private void OnProductsFetched(List products)
    {
        storeController.FetchPurchases();
    }

    private void OnProductsFetchFailed(ProductFetchFailed error)
    {
        Debug.Log($"Failed to fetch products - {error.FailureReason}");
    }

    private void OnStoreDisconnected(StoreConnectionFailureDescription failureDescription)
    {
        Debug.Log($"Store disconnected - {failureDescription.message}");
    }

    private bool ProcessPurchase(Order order)
    {
        string productId = order.Info.PurchasedProductInfo.Count > 0 ? order.Info.PurchasedProductInfo[0].productId : null;
        bool validReceipt = productId != null;

        try
            {
                var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);

                var result = validator.Validate(order.Info.Receipt);

            }
        catch (IAPSecurityException)
        {
            validReceipt = false;
        }

        if (validReceipt)
        {
            playerPurchases.Add(productId);
        }

        return validReceipt;
    }
}

The BuyProduct() function first checks whether the store has been initialized. If it has, it begins the purchase flow by calling storeController.PurchaseProduct() and passing in the ID of the item the user wants to buy. This method is marked public and static so it can be accessed from other parts of your project, for example from your shop UI.

That wraps up the coding portion of the IAP system. The only remaining step is to attach the script to a Unity GameObject.

Attach the Script

In the Unity Editor, right‑click in the Hierarchy and select Create Empty. Name it IAPManager.

Create empty menu.

Drag the IAPManager.cs script from the Assets folder onto the IAPManager GameObject to attach the script.

IAP manager game object.

When you run the scene, the IAP system will initialize automatically. In this example, we’re using a sample scene, but in a real project you would typically place the IAPManager object in your main menu or startup scene. Once initialization is complete, you can call the static methods in IAPManager.cs to check existing purchases or start a new purchase.

Conclusion

You now have everything you need to implement and initialize IAP in Unity. Keep in mind that Google and Apple occasionally require developers to update the Unity IAP package version. When this happens, Unity typically provides a migration guide to help you transition to the updated API. It’s best to follow their guidance and avoid relying on deprecated code, as outdated implementations can lead to bugs or compatibility issues in future releases.

Resources

If you would like to check Unity’s documentation, see Unity’s official IAP guide..

This website is not sponsored by or affiliated with Unity Technologies or its affiliates. “Unity” is a trademark or registered trademark of Unity Technologies or its affiliates in the U.S. and elsewhere.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top