# How to implement IAPs in Unity

**URL:** https://heroiclabs.com/docs/hiro/guides/unity/unity-iap/
**Keywords:** iap, in-app purchases, unity, hiro, unity purchasing, unitypurchasingsystem, storekit, storekit 2
**Categories:** hiro, unity, iap, guides

---


# How to implement IAPs in Unity

This guide shows you how to implement IAPs using Hiro's `UnityPurchasingSystem`, from economy configuration and Nakama credentials through to purchase handling and testing. 

## Why use Hiro for IAP in Unity

`UnityPurchasingSystem` builds on Unity's purchasing package and takes care of the parts of a purchase implementation your game code would otherwise have to manage:

<table class="no-col-borders">
  <tbody>
    <tr><td><strong>Product registration</strong></td><td>Reads your SKU-based store items from the Hiro economy config and registers them with Unity Purchasing automatically.</td></tr>
    <tr><td><strong>Platform store handling</strong></td><td>Routes purchases to the correct store (Apple App Store or Google Play) based on the player's platform.</td></tr>
    <tr><td><strong>Receipt validation</strong></td><td>Submits the receipt to Nakama for server-side validation after every platform purchase. Handles Unity IAP v4 and v5 API differences, and Apple StoreKit 2 validation.</td></tr>
    <tr><td><strong>Localized pricing</strong></td><td>Exposes real store prices in the player's locale and currency, populated directly from the platform store.</td></tr>
  </tbody>
</table>

## Before you begin

You'll need:
- [Unity Purchasing Package](https://docs.unity.com/en-us/iap) installed in your Unity editor, either v4 or v5.
- Hiro Unity SDK installed. See [Getting started](/hiro/concepts/getting-started/install/). If you are using Unity's IAP v5 package, you will also need the Hiro custom adapator for Unity IAP v5, available upon request.

## In this guide

1. [Create products on platform stores](#create-products-on-platform-stores)
2. [Configure IAP validation on Nakama](#configure-iap-validation-on-nakama)
3. [Configure store items in Hiro economy](#configure-store-items-in-hiro-economy)
4. [Initialize UnityPurchasingSystem](#initialize-unitypurchasingsystem)
5. [Purchase a store item](#purchase-a-store-item)
6. [Test your integration](#test-your-integration)

## Create products on platform stores

Each in-app product must be created in the platform store before it can be purchased. If you're shipping on both platforms, use the same product ID string in Apple App Store and Google Play.

{{< accordion title="Apple App Store" >}}

1. Sign in to [App Store Connect](https://appstoreconnect.apple.com) and select your app.
2. Go to **Features** > **In-App Purchases** and click **+**.
3. Select the purchase type: Consumable, Non-Consumable, or Auto-Renewable Subscription.
4. Enter a Product ID. This is the value you'll use as the `sku` in your Hiro economy config.
5. Complete the pricing, display name, and review screenshot required by Apple before the product can go live.

{{< /accordion >}}

{{< accordion title="Google Play" >}}

1. Sign in to [Google Play Console](https://play.google.com/console) and select your app.
2. Go to **Monetize** > **In-app products** (one-time purchases) or **Subscriptions** and click **Create product**.
3. Enter a Product ID. Use the same value as your Apple product ID if you're shipping on both platforms.
4. Set a price and activate the product.

{{< /accordion >}}

## Configure IAP validation on Nakama

Nakama validates purchase receipts against the platform stores server-side before granting rewards. Configure your Nakama server with the credentials it needs to contact each store's validation API before shipping any IAP items to production.

{{< accordion title="Apple App Store" >}}

The required Nakama configuration depends on which version of Unity IAP you're using:

| Unity IAP version | StoreKit version | Nakama config required |
| --- | --- | --- |
| v4 | StoreKit 1 | Yes — `iap.apple.shared_password` |
| v5 | StoreKit 2 | None — validated via PKI |

If you're using Unity IAP v4, generate an app-specific shared secret in App Store Connect and configure it in Nakama:

```yaml
iap:
  apple:
    shared_password: "your-shared-secret-here"
```

See [Validate Apple App Store purchases](/nakama/concepts/iap-validation/apple/) for step-by-step instructions.

{{< /accordion >}}

{{< accordion title="Google Play" >}}

Google Play validation requires a Google Cloud Service Account that has been granted access in Google Play Console. Download the service account JSON key and configure Nakama with the `client_email` and `private_key` values:

```yaml
iap:
  google:
    client_email: "nakama-iap-validator@your-project.iam.gserviceaccount.com"
    private_key: "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
```

See [Validate Google Play purchases](/nakama/concepts/iap-validation/google/) for full setup instructions, including creating the service account and granting the required permissions.

{{< /accordion >}}

{{< note "important" >}}
Configure IAP credentials before shipping. If a store item has a `sku` but Nakama cannot validate receipts, the purchase fails server-side after the player has already been charged by the platform store.
{{< /note >}}

## Configure store items in Hiro economy

Add a `sku` to any store item that requires a real-money purchase. The value must exactly match the product ID in App Store Connect or Google Play Console. See [Virtual Store](/hiro/concepts/economy/virtual-store/) for more store configuration details.

{{< accordion title="Example: base-economy.json" >}}

```json
{
    "initialize_user": {
        "currencies": {
            "Gems": 0
        }
    },
    "store_items": {
        "gem_pack_500": {
            "name": "500 Gems",
            "description": "A pack of 500 gems.",
            "category": "gem_packs",
            "cost": {
                "sku": "com.example.game.gems500"
            },
            "reward": {
                "guaranteed": {
                    "currencies": {
                        "Gems": {
                            "min": 500
                        }
                    }
                }
            }
        }
    },
    "placements": {},
    "donations": {}
}
```

{{< /accordion >}}

## Initialize UnityPurchasingSystem

`UnityPurchasingSystem` depends on `EconomySystem` and must be added to your system list after it.

Pass the store type into `EconomySystem` using `UnityPurchasingSystem.GetStoreType`. This maps `RuntimePlatform.Android` to `GooglePlay` and falls back to `AppleAppstore` for all other platforms. Then add `UnityPurchasingSystem` after it.

```csharp
var economySystem = new EconomySystem(logger, nakamaSystem,
    UnityPurchasingSystem.GetStoreType(Application.platform));
systems.Add(economySystem);

var purchasingSystem = new UnityPurchasingSystem(logger, economySystem);
systems.Add(purchasingSystem);
```

{{< note "important" >}}
**Product registration is automatic**. Hiro's `UnityPurchasingSystem` reads all SKU-based store items during `InitializeAsync` and registers them with Unity Purchasing. You don't call `UnityPurchasing.Initialize` yourself. It also populates `storeItem.Cost.LocalizedProductPrice` with real prices from the platform store.
{{< /note >}}

## Purchase a store item

You can purchase a store item defined in the Economy system.

```csharp
// Get a store item
var storeItem = economySystem.StoreItems.First();

var purchaseEventArgs = await purchasingSystem.BuyProductAsync(storeItem);
Debug.Log($"Receipt: {purchaseEventArgs.purchasedProduct.receipt}");
```

You can also purchase an item by product ID. Use the store item ID. For example, if your config defines `"store_items": { "gem_pack_500": { ... } }`, pass `"gem_pack_500"`.

```csharp
var purchaseEventArgs = await purchasingSystem.BuyProductByIdAsync("gem_pack_500");
```

`BuyProductAsync` triggers the platform's native purchase sheet. Once the player confirms, Unity Purchasing delivers the receipt to `UnityPurchasingSystem` internally. The system then calls `EconomySystem.PurchaseStoreItemAsync` with the receipt, waits for Nakama to validate and grant the reward, and only then confirms the purchase with the platform store. This ordering ensures the player can't receive a reward from a failed or duplicate transaction.

{{< accordion title="Unity IAP v5: Return Type change" >}}

With Unity IAP v5, `BuyProductAsync` returns `Task<IPurchaseResult>` instead of `Task<PurchaseEventArgs>`. This is a breaking change if your code reads from the result. The receipt is no longer directly exposed on the result. If you were accessing `result.purchasedProduct.receipt` (e.g. for analytics), use `result.Order` for store-level data instead.

| Method | v4 | v5 |
| --- | --- | --- |
| `BuyProductAsync` | `Task<PurchaseEventArgs>` | `Task<IPurchaseResult>` |
| `BuyProductByIdAsync` | `Task<PurchaseEventArgs>` | `Task<IPurchaseResult>` |

{{< /accordion >}}

## Handle purchase failures

`BuyProductAsync` and `BuyProductByIdAsync` throw `PurchaseFailureException` when the platform purchase fails. The exception's `Reason` property maps to Unity's `PurchaseFailureReason` enum.


{{< accordion title="Example: handle purchase failures" >}}

```csharp
private void HandlePurchaseFailure(PurchaseFailureException e)
{
    switch (e.Reason)
    {
        case PurchaseFailureReason.UserCancelled:
            // Player cancelled intentionally — no UI needed.
            break;
        case PurchaseFailureReason.PaymentDeclined:
            ShowErrorUI("Payment declined. Check your payment method and try again.");
            break;
        case PurchaseFailureReason.ExistingPurchasePending:
            ShowErrorUI("A purchase is already in progress.");
            break;
        case PurchaseFailureReason.PurchasingUnavailable:
        case PurchaseFailureReason.ProductUnavailable:
            ShowErrorUI("Purchasing is unavailable right now. Try again later.");
            break;
        case PurchaseFailureReason.SignatureInvalid:
        case PurchaseFailureReason.DuplicateTransaction:
        case PurchaseFailureReason.Unknown:
        default:
            ShowErrorUI("Something went wrong. Contact support if this continues.");
            break;
    }
}
```

{{< /accordion >}}

## Show localized prices

Use `GetLocalizedProductPrice` to display the real store price in the player's locale and currency. This data is populated from the platform store during initialization.

```csharp
string price = _purchasingSystem.GetLocalizedProductPrice(storeItem);
priceLabel.text = price; // e.g. "$0.99", "£0.79", "¥120"
```

You can also look up a price by product ID:

```csharp
string price = _purchasingSystem.GetLocalizedProductPriceById("gem_pack_500");
```

You can also read the localized price directly from the store item without going through the purchasing system:

```csharp
string price = storeItem.Cost.LocalizedProductPrice; // e.g. "$0.99", "£0.79", "¥120"
```

## Restore purchases (iOS)

Apple App Store guidelines require non-consumable purchases to be restorable. Add a "Restore Purchases" button to your settings UI and call `RestorePurchasesAsync` when tapped.

{{< accordion title="Example: restore purchases" >}}

```csharp
try
{
    await _purchasingSystem.RestorePurchasesAsync();
    ShowConfirmationUI("Purchases restored.");
}
catch (Exception e)
{
    ShowErrorUI($"Restore failed: {e.Message}");
}
```

{{< /accordion >}}

On non-Apple platforms (Android, PC, etc.), `RestorePurchasesAsync` returns immediately without doing anything. Restored transactions re-trigger the internal purchase processing, which re-validates receipts with Nakama.

{{< note "important" >}}
`RestorePurchasesAsync` only applies to non-consumable products. Consumable products (gem packs, coin bundles) aren't restorable by Apple's design.
{{< /note >}}

## Test your integration

### In the Unity Editor

Call `SetAllowFakeReceipts` on the economy system in your server code to enable fake receipt bypass for a given environment. With this enabled, receipts containing the string `fake receipt` or `FakeReceipt` bypass Nakama validation entirely, letting you test the purchase flow without real Apple or Google credentials.

```go
// main.go
if env == "dev" {
    systems.GetEconomySystem().SetAllowFakeReceipts(true)
}
```

`SetAllowFakeReceipts` defaults to `false`. Enable it only for non-production environments — never in a live build.

{{< note "important" >}}
The `allow_fake_receipts` field in the economy config JSON is deprecated. It remains in the config schema for backwards compatibility but has no effect. Use `SetAllowFakeReceipts` in server code instead.
{{< /note >}}

### On device

Use the platform's sandbox or test tooling:

- **Apple**: Sandbox testing with a Sandbox Apple ID, or TestFlight for pre-release builds
- **Google Play**: Internal test track with a licensed tester account

After a test purchase, check the **Purchases** section of your Nakama developer console to confirm the receipt reached the server and was validated successfully.
