Overview
brainCloud VerifyPurchase is used to ensure that in-app purchases are trusted, authoritative, and managed by the backend rather than the client. Based on the verified result, the backend can safely grant consumables, persist non-consumable entitlements, and manage subscription state and expirations. It is not just about rewarding items, but about making the server the single source of truth for what a user actually owns.
This guide covers the integration of Apple In-App Purchases using Unity IAP (StoreKit 2) alongside brainCloud’s server-side verification.
What's New in StoreKit 2?
JWS-based receipts - Signed JSON tokens replace legacy base64 receipts
Transaction IDs - Unique identifiers for each purchase (recommended for brainCloud verification)
Improved subscription management - Better renewal and cancellation tracking
Server-to-Server notifications - Real-time purchase updates from Apple
Apple App Store Connect Setup
Step 1: Create Your App in App Store Connect
Log in to App Store Connect
Navigate to My Apps → Click + → New App
Fill in required fields:
Platform: iOS
App Name
Primary Language
Bundle ID (must match Unity's Bundle Identifier)
SKU (unique identifier)
Step 2: Configure In-App Purchases
In your app's page, click In-App Purchases (left sidebar), or Subscriptions (for subscription purchases)
Click + to create a new in-app purchase or subscription
Creating Different Product Types
Consumable (e.g., Coins)
Type: Consumable
Reference Name: 100 Coins Pack
Product ID: coins_100
Price: Select tier
Non-Consumable (e.g., Remove Ads)
Type: Non-Consumable
Reference Name: Remove Ads
Product ID: remove_ads
Price: Select tier
Auto-Renewable Subscription (e.g., VIP Membership)
Type: Auto-Renewable Subscription
Subscription Group: Create new (e.g., "VIP Membership")
Reference Name: Monthly VIP
Product ID: vip_monthly
Subscription Duration: 1 Month
Price: Select tier
Non-Renewing Subscription (e.g., Event Pass)
Type: Non-Renewing Subscription
Reference Name: 7-Day Event Pass
Product ID: event_pass_7days
Price: Select tier
Step 3: Configure Sandbox Testers
Go to App Store Connect → Users and Access → Sandbox tab
Click + to add a tester if you don't have any
Create test Apple IDs (use unique, non-existing email addresses)
Save tester credentials for testing
Step 4: Configure purchase integration keys
Navigate to App Store Connect → Users and Access → Integrations tab
Generate an In-App Purchase key for consumable and non-consumable products. Record the Issuer ID, Key ID, and the contents of the downloaded encoded key for configuration within the brainCloud portal.
Generate a Shared Secret for subscription purchases if required for your implementation.
Unity Project Setup
Installation
Unity IAP: Package Manager → Unity Registry →
In-App Purchasingservice (v5.1.2+)brainCloud SDK: Package Manager → Add from GitHub URL
Configuration
iOS Player Settings:
Bundle Identifier must match App Store Connect
Target iOS 15.0+ for StoreKit 2 support
Step 1: Install Unity IAP
Open Unity Editor
Go to Window → Package Manager
Unity Registry → In-App Purchasing
Install version 5.1.2 or newer
Step 2: Install brainCloud SDK
Open Unity Editor
Go to Window → Package Manager
Click + → Install package from git URL...
pass
https://github.com/getbraincloud/braincloud-unity-package.git
Step 3: Configure Unity Project Settings
iOS Player Settings
File → Build Settings → iOS → Player Settings
Player Settings:
├── Company Name: [Your Company]
├── Product Name: [Your App Name]
└── Bundle Identifier: com.yourcompany.yourapp (must match App Store Connect)
Other Settings:
├── Target minimum iOS Version: 15.0 or higher
├── Architecture: ARM64
└── Automatically Sign: Enable (or configure manual signing)
Identification:
└── Signing Team ID: [Your Apple Team ID]
brainCloud Configuration
Step 1: Create brainCloud Application
Log in to brainCloud Portal
Create a new app or use an existing one
From App > Design > Core App Info page, note your app credentials and apply them to your Unity project
Navigate to the
Appletab under theConfigure Platformssection and complete the required fields:Bundle ID must match your app identifier in your Apple Developer account and Unity's Bundle Identifier
Enter the In-App Purchase Shared Secret (required only for auto-renewable subscriptions)
Enter the Apple ID for your application, which can be found in App Store Connect under General → App Information section
Enter the Issuer ID, Key ID, and encoded key from the
Configure purchase integration keysstepLeave the
Use App Store Server API for legacy receipts (optional)checkbox unchecked if you are not using the old receipt-based IAP system, instead of a transaction-based system
Step 2: Configure Products in brainCloud
Critical: Products Identifiers in brainCloud must exactly match your Apple App Store Connect products.
In brainCloud portal, navigate to: Design → Marketplace → Products
Click + Add Product for each IAP item
Product Configuration Fields
For each product, configure:
Product ID (Required)
Must match Unity product constant (e.g.,
coins_100)This is your internal identifier
iTunes Product ID (Critical for Apple)
Must exactly match the Product ID in App Store Connect
This is what brainCloud uses to verify with Apple
Case-sensitive!
Product Type:
Consumable - Can be purchased multiple times (coins, lives, etc.)
Non-Consumable - One-time purchase (remove ads, unlock features)
Subscription - Use for both auto-renewable and non-renewing subscriptions
Category:
Currency - For virtual currency (coins, gems)
Item - For consumable items (power-ups, boosts)
Unlock - For unlockable content (levels, characters)
Price: Enter the price in your default currency (for reference/analytics)
Example: Consumable Product (Coins)
Product ID: coins_100
iTunes Product ID: coins_100
Type: Consumable
Category: Currency
Price: 0.99
Description: 100 gold coins
Example: Non-Consumable Product (Remove Ads)
Product ID: remove_ads
iTunes Product ID: remove_ads
Type: Non-Consumable
Category: Unlock
Price: 2.99
Description: Remove all advertisements
Example: Subscription Product (VIP Monthly)
Product ID: vip_monthly
iTunes Product ID: vip_monthly
Type: Subscription
Category: Unlock
Price: 9.99
Description: Monthly VIP membership
Important Notes:
You can also configure product data (e.g., how many coins to grant) in the brainCloud product definition
This allows you to change rewards server-side without app updates
Use the Data field (JSON) to store product-specific configuration
Adding Product Data (Optional but Recommended)
In the Data field, you can add JSON configuration:
{
"coins": 100,
"bonus_coins": 10,
"icon": "coin_pack_100"
}This data is returned in the VerifyPurchase response, allowing server-controlled content delivery.
In the brainCloud portal, go to Design → Marketplace → Products
Click + to add products matching your App Store Connect products:
Product ID: coins_100
Type: Consumable
Category: Currency
Price: [Match App Store pricing]
iTunes Product ID: coins_100 (must match exactly)
Repeat for all products:
coins_100(Consumable)remove_ads(Non-Consumable)vip_monthly(Subscription)event_pass_7days(Subscription)
Code Implementation Guide
Step 1: Script Setup and Initialization
Create a new C# script (e.g., IAPManager.cs) and attach to a GameObject in your scene.
Core Components Declaration
using UnityEngine;
using UnityEngine.Purchasing;
using BrainCloud;
using System.Collections.Generic;
using System.Linq;
public class IAPManager : MonoBehaviour
{
// Unity IAP Controller
private StoreController storeController;
// brainCloud Wrapper
private BrainCloudWrapper bc;
// Receipt tracking for verification
private string lastReceipt;
private string lastTransactionId;
// Product IDs (must match App Store Connect)
public const string COINS = "coins_100";
public const string REMOVE_ADS = "remove_ads";
public const string VIP = "vip_monthly";
public const string EVENT_PASS = "event_pass_7days";
}
Key Points:
StoreControlleris the new Unity IAP v5 controller (replaces the oldIStoreController)Product IDs must exactly match App Store Connect Product IDs
lastTransactionIdcaptures StoreKit 2 transaction IDs for verification
Step 2: brainCloud Initialization
void InitBrainCloud()
{
// Get or add BrainCloudWrapper component
bc = gameObject.GetComponent<BrainCloudWrapper>();
if (bc == null)
{
bc = gameObject.AddComponent<BrainCloudWrapper>();
}
bc.WrapperName = gameObject.name;
// Initialize with your brainCloud credentials
bc.Init(
"<REDACTED> // Server URL
"YOUR_APP_ID", // Your App ID
"YOUR_APP_SECRET", // Your App Secret
"1.0.0" // App Version
);
bc.Client.EnableLogging(true); // Enable for debugging
bc.SetAlwaysAllowProfileSwitch(true);
Debug.Log("brainCloud initialized");
}
Replace placeholders:
YOUR_APP_ID: From the brainCloud portalYOUR_APP_SECRET: From the brainCloud portal
Call in Start():
void Start()
{
InitBrainCloud();
InitializePurchasing();
}
Step 3: Unity IAP Initialization
async void InitializePurchasing()
{
// Create store controller instance
storeController = UnityIAPServices.StoreController();
// Subscribe to purchase events
storeController.OnStoreDisconnected += OnStoreDisconnected;
storeController.OnProductsFetched += OnProductsFetched;
storeController.OnProductsFetchFailed += OnProductsFetchFailed;
storeController.OnPurchasePending += OnPurchasePending;
storeController.OnPurchaseConfirmed += OnPurchaseConfirmed;
storeController.OnPurchaseFailed += OnPurchaseFailed;
storeController.OnPurchaseDeferred += OnPurchaseDeferred;
// Connect to store (async operation)
await storeController.Connect();
// Fetch product information
FetchProducts();
}
void FetchProducts()
{
// Define products to fetch from App Store
var products = new List<ProductDefinition>
{
new(COINS, ProductType.Consumable),
new(REMOVE_ADS, ProductType.NonConsumable),
new(VIP, ProductType.Subscription),
new(EVENT_PASS, ProductType.Subscription)
};
storeController.FetchProducts(products);
}
Important:
await storeController.Connect()is asynchronous - make methodasync voidProduct types must match App Store configuration
FetchProducts()retrieves localized pricing and metadata from Apple
Step 4: Purchase Flow Implementation
Initiating a Purchase
public void Buy(string productId)
{
if (storeController == null)
{
Debug.LogError("IAP not initialized");
return;
}
// Trigger purchase flow
storeController.PurchaseProduct(productId);
}
Usage example:
// Call from UI button
Buy(COINS);
Handling Purchase Pending
void OnPurchasePending(PendingOrder order)
{
var product = GetFirstProductInOrder(order);
if (product == null)
{
Debug.LogError("Product not found in pending order");
return;
}
// Extract receipt (JWS for StoreKit 2)
lastReceipt = ExtractReceipt(order);
// Extract transaction ID (StoreKit 2 specific)
if (!string.IsNullOrEmpty(order.Info.TransactionID))
{
lastTransactionId = order.Info.TransactionID;
}
Debug.Log($"Purchase pending: {product.definition.id}");
Debug.Log($"Transaction ID: {lastTransactionId}");
// Confirm purchase (moves to OnPurchaseConfirmed)
storeController.ConfirmPurchase(order);
}
StoreKit 2 Key Feature:
order.Info.TransactionIDprovides unique transaction identifierThis ID is used for brainCloud verification
Receipt contains JWS signature instead of legacy base64
Extracting Receipt Data
static string ExtractReceipt(Order order)
{
// Try StoreKit 2 JWS first
var jws = order.Info.Apple?.jwsRepresentation;
if (!string.IsNullOrEmpty(jws))
{
return jws;
}
// Fallback to legacy receipt
return order.Info.Receipt;
}
Step 5: Purchase Confirmation
void OnPurchaseConfirmed(Order order)
{
switch (order)
{
case ConfirmedOrder confirmedOrder:
{
var product = GetFirstProductInOrder(confirmedOrder);
// Store transaction ID
if (!string.IsNullOrEmpty(confirmedOrder.Info.TransactionID))
{
lastTransactionId = confirmedOrder.Info.TransactionID;
}
Debug.Log($"Purchase confirmed: {product?.definition.id}");
Debug.Log($"Transaction ID: {lastTransactionId}");
// Grant content to player here
GrantPurchaseContent(product);
break;
}
case FailedOrder failedOrder:
{
var product = GetFirstProductInOrder(failedOrder);
Debug.LogError($"Confirmation failed: {product?.definition.id}");
Debug.LogError($"Reason: {failedOrder.FailureReason}");
break;
}
}
}
void GrantPurchaseContent(Product product)
{
// Implement your content granting logic
switch (product.definition.id)
{
case COINS:
// Add 100 coins to player
Debug.Log("Granted 100 coins");
break;
case REMOVE_ADS:
// Disable ads permanently
Debug.Log("Ads removed");
break;
case VIP:
// Activate VIP subscription
Debug.Log("VIP activated");
break;
case EVENT_PASS:
// Activate event pass
Debug.Log("Event pass activated");
break;
}
}
Step 6: brainCloud Receipt Verification
Authentication First
void AuthenticateBrainCloud()
{
if (bc == null)
{
Debug.LogError("brainCloud not initialized");
return;
}
bc.AuthenticateUniversal(
"user_unique_id", // User ID (can be device ID, custom ID, etc.)
"user_password", // Password (or generate random for anonymous)
true, // Create account if doesn't exist
// Success callback
(json, cb) =>
{
Debug.Log("brainCloud authenticated successfully");
Debug.Log($"Response: {json}");
},
// Error callback
(status, reason, error, cb) =>
{
Debug.LogError($"brainCloud auth failed: {reason}");
Debug.LogError($"Error: {error}");
}
);
}
Call before verifying purchases:
// Authenticate once at app start or before first purchase
AuthenticateBrainCloud();
Verify Purchase with Transaction ID (Recommended for StoreKit 2)
void VerifyLastPurchase()
{
if (bc == null || !bc.Client.Authenticated)
{
Debug.LogError("brainCloud not authenticated");
return;
}
// Prefer transaction ID verification (StoreKit 2)
if (!string.IsNullOrEmpty(lastTransactionId))
{
Debug.Log("Verifying with StoreKit 2 transaction ID...");
string storeId = "itunes";
// Format receipt data with transaction ID
string receiptData = $"{{\"transactionId\":\"{lastTransactionId}\",\"excludeOldTransactions\":false}}";
bc.AppStoreService.VerifyPurchase(
storeId,
receiptData,
// Success callback
(json, cb) =>
{
Debug.Log("✅ Purchase verified successfully!");
Debug.Log($"Response: {json}");
// Parse response and grant rewards server-side if needed
HandleVerificationSuccess(json);
},
// Error callback
(status, reason, error, cb) =>
{
Debug.LogError($"❌ Verification failed: {reason}");
Debug.LogError($"Error details: {error}");
// Handle verification failure
HandleVerificationFailure(reason, error);
}
);
return;
}
// Fallback: Verify with full receipt (legacy/StoreKit 1)
if (!string.IsNullOrEmpty(lastReceipt))
{
Debug.Log("Verifying with receipt data...");
string storeId = "itunes";
bc.AppStoreService.VerifyPurchase(
storeId,
lastReceipt, // Full JWS or legacy receipt
(json, cb) =>
{
Debug.Log("✅ Purchase verified!");
Debug.Log(json);
},
(status, reason, error, cb) =>
{
Debug.LogError($"❌ Verification failed: {reason}");
Debug.LogError(error);
}
);
return;
}
Debug.LogError("No receipt or transaction ID available");
}
Handling Verification Response
void HandleVerificationSuccess(string jsonResponse)
{
// Parse brainCloud response
// Response contains:
// - Transaction details
// - Product information
// - Verification status
Debug.Log("Purchase verified by brainCloud server");
// You can now safely grant premium content
// brainCloud will prevent duplicate grants automatically
}
void HandleVerificationFailure(string reason, string error)
{
// Handle different failure scenarios
if (reason.Contains("duplicate"))
{
Debug.Log("Purchase already verified (duplicate)");
// Already granted, no action needed
}
else if (reason.Contains("invalid"))
{
Debug.LogError("Invalid receipt - possible fraud attempt");
// Do NOT grant content
}
else
{
Debug.LogError("Verification error - retry later");
// Queue for retry
}
}
Step 7: Error Handling
void OnPurchaseFailed(FailedOrder order)
{
var product = GetFirstProductInOrder(order);
// Handle duplicate transactions gracefully
if (order.FailureReason == PurchaseFailureReason.DuplicateTransaction)
{
Debug.LogWarning($"Duplicate transaction: {product?.definition.id}");
// Already processed, ignore
return;
}
Debug.LogError($"Purchase failed: {product?.definition.id}");
Debug.LogError($"Reason: {order.FailureReason}");
Debug.LogError($"Details: {order.Details}");
// Show user-friendly error message
ShowErrorToUser($"Purchase failed: {order.FailureReason}");
}
void OnPurchaseDeferred(DeferredOrder order)
{
var product = GetFirstProductInOrder(order);
Debug.LogWarning($"Purchase deferred (requires parental approval): {product?.definition.id}");
// Inform user to check with parent/guardian
ShowMessageToUser("Purchase requires approval. Please check later.");
}
void OnStoreDisconnected(StoreConnectionFailureDescription description)
{
Debug.LogError($"Store connection failed: {description.message}");
// Attempt reconnection or notify user
ShowErrorToUser("Cannot connect to App Store. Please check internet connection.");
}
Step 8: Utility Helper Methods
// Extract first product from order
static Product GetFirstProductInOrder(Order order)
{
return order.CartOrdered.Items().FirstOrDefault()?.Product;
}
// Detailed logging for debugging
void LogOrderDetails(string stage, Order order, Product product)
{
var info = order.Info;
var jws = info.Apple?.jwsRepresentation;
var storeName = info.Apple?.StoreName ?? "Unknown";
Debug.Log($" Order {stage}:");
Debug.Log($" Product: {product?.definition.id}");
Debug.Log($" Type: {product?.definition.type}");
Debug.Log($" Transaction ID: {info.TransactionID}");
Debug.Log($" Store: {storeName}");
Debug.Log($" Has Receipt: {!string.IsNullOrEmpty(info.Receipt)}");
Debug.Log($" Has JWS: {!string.IsNullOrEmpty(jws)}");
}
Test and monitor the log from brainCloud
Use your sandbox tester email when testing purchases.
Review the results in the user logs on the brainCloud portal.
And transaction details from the User Transactions page












