Introduction
Monetizing your game effectively requires a robust In-App Purchasing (IAP) implementation. Unity IAP version 5 provides a unified, service-based workflow for building cross-platform purchases across Google Play and the Apple App Store, completely replacing the older
IStoreListener architecture. This guide covers the essentials of modern Unity IAP 5 implementation, from initialization to secure purchase fulfillment. Note that specific API details may vary slightly depending on your exact Unity IAP package version.Store and Product Configuration
Before writing code, you must configure your products in both the Unity Editor and the respective store developer consoles:
- Store Dashboards: You must create your app in the Google Play Console and Apple App Store Connect, set up merchant accounts, and define your products.
- Product IDs: The Product IDs you use in your Unity C# code must match the IDs defined in the store dashboards exactly.
- Unity IAP Catalog: While optional, using the Unity IAP Catalog window helps centralize your product definitions.
- Store Dashboards: You must create your app in the Google Play Console and Apple App Store Connect, set up merchant accounts, and define your products.
- Product IDs: The Product IDs you use in your Unity C# code must match the IDs defined in the store dashboards exactly.
- Unity IAP Catalog: While optional, using the Unity IAP Catalog window helps centralize your product definitions.
Initializing Unity IAP 5
The modern approach uses
Use the provided failure and disconnection events (like
UnityIAPServices.StoreController() to manage the lifecycle of your storefront. To initialize, you first initialize Unity Services, subscribe to the necessary StoreController events (such as OnProductsFetched and OnPurchasePending), and then call Connect(). Once connected, you can fetch your product definitions and any existing purchases the user has.Use the provided failure and disconnection events (like
OnPurchasesFetchFailed or OnStoreDisconnected) to handle network issues gracefully.Complete Purchase Flow
The v5 API relies heavily on an event-driven flow. When a user buys something,
Here is a complete, modern implementation:
OnPurchasePending fires. You must deliver the digital goods to the user. Only after successful delivery should you call ConfirmPurchase(). If your game crashes before confirmation, the pending order will be redelivered the next time IAP initializes. Therefore, your fulfillment logic must be idempotent (safe to run multiple times without double-rewarding the user).Here is a complete, modern implementation:
infoIf you crash or close the app before calling ConfirmPurchase, the OnPurchasePending event will typically re-invoke the next time Unity IAP initializes, allowing you to recover the transaction.
csharp
using System.Collections.Generic;
using Unity.Services.Core;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;
using UnityEngine.Purchasing.Models;
public class IAPManager : MonoBehaviour
{
private StoreController _storeController;
private string _coinProductID = "com.yourgame.coins.100";
async void Start()
{
await UnityServices.InitializeAsync();
InitializeIAP();
}
private void InitializeIAP()
{
_storeController = UnityIAPServices.StoreController();
// 1. Subscribe to the v5 events
_storeController.OnProductsFetched += OnProductsFetched;
_storeController.OnPurchasesFetched += OnPurchasesFetched;
_storeController.OnPurchasesFetchFailed += OnPurchasesFetchFailed;
_storeController.OnPurchasePending += OnPurchasePending;
_storeController.OnPurchaseConfirmed += OnPurchaseConfirmed;
_storeController.OnPurchaseFailed += OnPurchaseFailed;
_storeController.OnPurchaseDeferred += OnPurchaseDeferred;
_storeController.OnStoreDisconnected += OnStoreDisconnected;
// 2. Connect to the store
_storeController.Connect().ContinueWith(task => {
if (task.IsCompletedSuccessfully) {
var products = new List<ProductDefinition> {
new ProductDefinition(_coinProductID, ProductType.Consumable)
};
// 3. Fetch the product catalog
_storeController.FetchProducts(products);
}
});
}
private void OnProductsFetched(List<Product> products)
{
Debug.Log("Catalog Products fetched successfully.");
// 4. Fetch past purchases after the catalog is ready
_storeController.FetchPurchases();
}
private void OnPurchasesFetched(Orders orders)
{
Debug.Log("Past purchases fetched. Restoring non-consumables...");
// Loop through orders.ConfirmedOrders and restore entitlements if necessary
}
private void OnPurchasesFetchFailed(PurchasesFetchFailureDescription description)
{
Debug.LogError($"Failed to fetch purchases: {description.reason}");
}
private void OnPurchasePending(PendingOrder order)
{
Debug.Log("Purchase pending! Granting reward...");
// 5. Fulfill the order. Ensure logic is idempotent!
bool success = GrantReward(order.ProductId, order.TransactionId);
if (success) {
// 6. Only confirm the purchase AFTER successful fulfillment
_storeController.ConfirmPurchase(order);
}
}
private void OnPurchaseConfirmed(Order order)
{
// Confirmation can fail. Check the actual state rather than assuming success.
if (order.State == OrderState.Confirmed) {
Debug.Log("Purchase successfully confirmed with the store.");
} else {
Debug.LogWarning($"Confirmation was not successful. State: {order.State}");
}
}
private void OnPurchaseFailed(FailedOrder order)
{
Debug.LogWarning($"Purchase failed: {order.FailureReason}");
// Update UI to inform the user
}
private void OnPurchaseDeferred(DeferredOrder order)
{
Debug.Log("Purchase deferred (e.g., Ask to Buy on iOS). Waiting for parental approval.");
}
private void OnStoreDisconnected(StoreConnectionFailureDescription description)
{
Debug.LogWarning($"Store disconnected: {description.reason}");
}
public void BuyCoins()
{
_storeController.PurchaseProduct(_coinProductID);
}
private bool GrantReward(string productId, string transactionId)
{
// Check if transactionId was already rewarded, if not, grant it
return true;
}
}Consumables vs Non-Consumables vs Subscriptions
You must categorize your products into three types:
- Consumables: Items that are destroyed upon use (e.g., coins, potions). Because users expect to keep their currency if they switch devices, consumable balances should ideally be persisted remotely (e.g., in a cloud database) rather than relying solely on local device storage.
- Non-Consumables: Items purchased only once (e.g., "Remove Ads", unlock full game). These are tied to the user's store account permanently. You must restore these entitlements when the user installs the app on a new device.
- Subscriptions: Recurring purchases that grant access to content over a specific duration. The store handles the billing, but your game must verify the active status of the subscription upon startup.
- Consumables: Items that are destroyed upon use (e.g., coins, potions). Because users expect to keep their currency if they switch devices, consumable balances should ideally be persisted remotely (e.g., in a cloud database) rather than relying solely on local device storage.
- Non-Consumables: Items purchased only once (e.g., "Remove Ads", unlock full game). These are tied to the user's store account permanently. You must restore these entitlements when the user installs the app on a new device.
- Subscriptions: Recurring purchases that grant access to content over a specific duration. The store handles the billing, but your game must verify the active status of the subscription upon startup.
Restore Purchases
Apple strictly requires applications to provide a mechanism to restore non-consumable purchases. If a user deletes your app and reinstalls it, they should not have to buy the "Remove Ads" upgrade again.
Platform approaches differ. Google Play restoration is handled through the IAP initialization and
Note: The
Platform approaches differ. Google Play restoration is handled through the IAP initialization and
FetchPurchases() flow, which retrieves the user's current purchase information. On Apple platforms, a user-facing Restore Purchases button should use _storeController.RestoreTransactions(...). After the Apple restore/resync operation is complete, Unity IAP fetches purchases and invokes OnPurchasesFetched.Note: The
OnPurchasesFetched(Orders) event can contain Pending, Confirmed, and Deferred orders. Any pending orders must still be handled through your standard purchase fulfillment flow rather than blindly assuming they are fully confirmed.Purchase Validation and Security
For low-stakes games, trusting the client's
For high-risk items, you should implement Server-Side Verification. Instead of immediately granting the item locally, the client sends the receipt data to your secure backend. The backend should verify the platform purchase/receipt data using the appropriate store-side verification mechanism before granting high-value entitlements. Only after the backend confirms validity should it update the player's inventory and instruct the client to call
OnPurchasePending event to grant items locally is often acceptable. However, for valuable purchases in competitive or server-authoritative games, client-side logic is vulnerable to piracy (e.g., modified APKs or tools that forge receipts).For high-risk items, you should implement Server-Side Verification. Instead of immediately granting the item locally, the client sends the receipt data to your secure backend. The backend should verify the platform purchase/receipt data using the appropriate store-side verification mechanism before granting high-value entitlements. Only after the backend confirms validity should it update the player's inventory and instruct the client to call
ConfirmPurchase(). Granting premium currency solely from untrusted client state is unsafe.Handling Failed, Deferred, and Offline States
A robust IAP system gracefully handles failures. You must respond appropriately to the following events:
- OnPurchaseFailed: Triggers if the user cancels the payment dialogue, their card declines, or an error occurs. Update your UI to clear any loading spinners and optionally notify the user.
- OnPurchaseDeferred: Commonly used on iOS for "Ask to Buy" when a child requests a purchase. The transaction is paused until a parent approves it remotely. Update your UI to inform the user they must wait for approval.
- OnPurchasesFetchFailed / OnStoreDisconnected: Handle network outages by allowing the user to retry connecting or disabling the storefront UI entirely.
Remember that calling
- OnPurchaseFailed: Triggers if the user cancels the payment dialogue, their card declines, or an error occurs. Update your UI to clear any loading spinners and optionally notify the user.
- OnPurchaseDeferred: Commonly used on iOS for "Ask to Buy" when a child requests a purchase. The transaction is paused until a parent approves it remotely. Update your UI to inform the user they must wait for approval.
- OnPurchasesFetchFailed / OnStoreDisconnected: Handle network outages by allowing the user to retry connecting or disabling the storefront UI entirely.
Remember that calling
ConfirmPurchase() itself can fail if the network drops during the request. OnPurchaseConfirmed provides an Order whose actual state should be checked; do not assume the callback alone guarantees successful acknowledgement. Keep track of the pending order so you can attempt confirmation again later if needed.Testing Without Real Purchases
You cannot fully reproduce store billing behavior or receipt generation in a normal Unity Editor play session. To accurately test your flow without spending real money, you must use platform-specific sandbox environments:
- Google Play: Add your tester email to the License Testers list in the Play Console, and upload your build to an Internal Test Track. (Tip: You can reduce your Unity mobile build size to speed up these iterative test deployments.)
- iOS Sandbox: Create a Sandbox Tester account in App Store Connect and sign into your test device with it.
Ensure your application ID and Product IDs match exactly. You must test the complete lifecycle of consumables, non-consumables, and subscriptions separately, as they behave differently.
- Google Play: Add your tester email to the License Testers list in the Play Console, and upload your build to an Internal Test Track. (Tip: You can reduce your Unity mobile build size to speed up these iterative test deployments.)
- iOS Sandbox: Create a Sandbox Tester account in App Store Connect and sign into your test device with it.
Ensure your application ID and Product IDs match exactly. You must test the complete lifecycle of consumables, non-consumables, and subscriptions separately, as they behave differently.
Common Android and iOS Problems
- Product Unavailable: Often means the Product ID in your code doesn't exactly match the dashboard, the app isn't published to an active track, or the product is inactive.
- Incorrect Tester Account: Ensure the device is logged into the exact Google/Apple account registered as a tester.
- Store Connection Failures: Often caused by Android build configuration errors, missing billing permissions, or network issues.
- Missing App Configuration: Your game must be appropriately signed and uploaded to the respective store console at least once before IAP will initialize properly on a device.
- Incorrect Tester Account: Ensure the device is logged into the exact Google/Apple account registered as a tester.
- Store Connection Failures: Often caused by Android build configuration errors, missing billing permissions, or network issues.
- Missing App Configuration: Your game must be appropriately signed and uploaded to the respective store console at least once before IAP will initialize properly on a device.
Best Practices
- Fetch products from the catalog before allowing purchases.
- Fetch existing purchases to restore non-consumables.
- Fulfill the digital reward before calling
- Make your fulfillment logic idempotent to survive crashes during the transaction.
- Persist consumable balances remotely to prevent data loss.
- Validate valuable purchases server-side.
- Create sensible UI states for failed, deferred, and offline scenarios.
- Test comprehensively on both Android and iOS devices using sandbox accounts.
- Fetch existing purchases to restore non-consumables.
- Fulfill the digital reward before calling
ConfirmPurchase.- Make your fulfillment logic idempotent to survive crashes during the transaction.
- Persist consumable balances remotely to prevent data loss.
- Validate valuable purchases server-side.
- Create sensible UI states for failed, deferred, and offline scenarios.
- Test comprehensively on both Android and iOS devices using sandbox accounts.
Frequently Asked Questions
Do I need Unity Gaming Services to use Unity IAP?
Yes, the modern v5 API relies on the Unity Services Core package for initialization before you can connect to the store.
What is the difference between FetchPurchases() and GetPurchases()?
Why is my product unavailable?
This usually indicates a mismatch between your Product ID in Unity and the store dashboard, or your test device isn't properly registered as a licensed tester.
When should I call ConfirmPurchase()?
Exactly once, immediately after you have successfully and securely delivered the digital item to the player.
How do I restore purchases on iOS?
Provide a "Restore Purchases" button that calls
Can I test IAP in the Unity Editor?
The Editor provides a basic Fake Store for testing UI flows, but it does not generate real receipts or communicate with Google/Apple. You must build to a device for accurate testing.
Yes, the modern v5 API relies on the Unity Services Core package for initialization before you can connect to the store.
What is the difference between FetchPurchases() and GetPurchases()?
FetchPurchases() asks the store for the user's current purchase information and updates the purchases available through GetPurchases(), returning results via OnPurchasesFetched. GetPurchases() synchronously retrieves the last known, locally cached state of purchases, which may be outdated.Why is my product unavailable?
This usually indicates a mismatch between your Product ID in Unity and the store dashboard, or your test device isn't properly registered as a licensed tester.
When should I call ConfirmPurchase()?
Exactly once, immediately after you have successfully and securely delivered the digital item to the player.
How do I restore purchases on iOS?
Provide a "Restore Purchases" button that calls
_storeController.RestoreTransactions(...). Once the Apple resync is complete, Unity IAP will automatically fetch purchases and trigger OnPurchasesFetched.Can I test IAP in the Unity Editor?
The Editor provides a basic Fake Store for testing UI flows, but it does not generate real receipts or communicate with Google/Apple. You must build to a device for accurate testing.
