Skip to main content

How CachePurchasePayloadContext() works for In-App Purchases

How to cache the brainCloud purchase context before a Google Play, Apple, or Meta Horizon purchase with examples

Written by Jason Liang

Overview

AppStore.CachePurchasePayloadContext() was introduced in brainCloud 5.8. It tells brainCloud exactly which brainCloud product, price and promotion a user is about to buy, before the store purchase begins. When the receipt is later sent to VerifyPurchase(), brainCloud uses this cached context to award the correct product.

This call is optional and is not tied to StoreKit 2 or to any particular store SDK version. It doesn't change how purchases are made on the store side, and you can still process purchases the traditional way. What it adds is more flexibility and better control over how store purchases are mapped to in-game items.

Why you need it

The limitation: receipts only carry a product ID

Apple and Google receipts only return the store product ID (plus some basic transaction details). They don't say which in-game item, bundle or offer the user picked in your shop.

Apple and Google don't strictly force "one fixed price per product ID forever". You can change the price tier of a product in their consoles, and you don't always need a brand-new product ID to run a discount. The real problem is on the reward side. If you want different items or offers to share one product ID, the store alone can't tell you what to grant.

As a result, developers often encode both the item and the price into the product ID, which quickly multiplies the number of IAP IDs to manage:

apple_199
orange_199
banana_199
apple_299
orange_299
...

This makes store configuration harder to maintain. It also reduces flexibility when you want to reuse price points across different items, or change offers and promotions.

The solution: map product IDs to price points and cache the context

With CachePurchasePayloadContext(), you treat store products primarily as generic price points rather than individual items, for example a single fruit_199 product for everything priced at $1.99:

Store product ID   →  Price point
fruit_199 → $1.99
fruit_299 → $2.99

Just before starting the purchase, the client caches the extra context (the brainCloud payload) that says which item or bundle the user selected. When the receipt comes back with only fruit_199, brainCloud combines it with the cached payload to know exactly what to reward: apple, orange or banana.

This lets you:

  • Reuse product IDs across multiple items and promotions, and avoid an explosion of store product IDs.

  • Simplify store setup: create and approve fewer IAP products in App Store Connect, the Google Play Console and the Meta Horizon dashboard.

  • Support dynamic offers and promotions configured in brainCloud, without creating new store products.

  • Avoid wrong rewards when several brainCloud products share a store product ID.

Some stores used to let developers attach a "developer payload" to a purchase so this context could travel inside the receipt. Apple never supported this. Google Play has deprecated it, and Meta Horizon doesn't support it. CachePurchasePayloadContext keeps the context on the brainCloud side instead, so it works the same way across these stores.

How it works

  1. The client calls GetSalesInventory(). Each product returned includes a payload string, for example __bc:[1,apple_bundle,1,199,-1].

  2. When the user taps Buy, the client calls CachePurchasePayloadContext(storeId, iapId, payload) with that product's payload right before starting the native store purchase.

  3. The client completes the purchase with the store (Google Play Billing, StoreKit or the Meta Horizon Platform SDK).

  4. The client calls VerifyPurchase(storeId, receiptData) right away.

  5. If the receipt doesn't carry a brainCloud payload, brainCloud looks up the cached context for the same app + user + storeId + iapId, created around the time of verification. It then uses that context to determine the product, price and promotion to award.

Parameters

Parameter

Description

storeId

The brainCloud store ID: googlePlay, itunes or metaHorizon. Note that Apple is itunes, not apple.

iapId

The store's own product ID. It must exactly match the ID in the receipt: Google Play productId, Apple product_id, Meta Horizon sku.

payload

The payload string returned for that product by GetSalesInventory(). Pass it through unchanged.

The call returns an empty success response.

Payload format

The payload is generated by brainCloud and should be treated as opaque. For reference, its format is:

__bc:[versionId,itemId,priceId,referencePrice,promotionId]
__bc:[versionId,itemId,priceId,referencePrice,promotionId,campaignScenarioCode]

A promotionId of -1 means no promotion.

Timing matters

brainCloud matches a cached context to a receipt using the time of the VerifyPurchase call. By default, the context must have been cached no more than 60 seconds before VerifyPurchase is called (with a 5-second tolerance after). For this reason:

  • Call CachePurchasePayloadContext immediately before starting the store purchase, not when the shop screen opens.

  • Call VerifyPurchase as soon as the store returns a successful purchase.

Cached entries expire after 4 days by default.

Supported stores

Store

storeId

Uses cached context?

Notes

Apple App Store

itunes

✅ Yes

Apple receipts never contain a developer payload, so the cached context is always used.

Google Play

googlePlay

✅ Yes

If the receipt includes a brainCloud developerPayload, that is used first. Otherwise the cached context is used, which is the recommended approach.

Meta Horizon (Quest)

metaHorizon

✅ Yes

Meta Horizon doesn't support developer payloads, so the cached context is the only way to pass the brainCloud context. Requires brainCloud 5.9 or later.

Stores that don't use it

The following stores accept the call without error, but ignore the cached context during verification. Calling it for these stores has no effect:

  • Amazon (amazon)

  • Steam (steam)

  • Facebook (facebook)

  • Windows / Microsoft Store (windows, windowsPhone)

  • Epic Games (epicGames)

  • Xsolla (xsolla): Xsolla carries the brainCloud payload in its own custom purchase data, so caching isn't needed.

These stores have different purchase flows (server-side or store-managed transactions), so you can keep using your existing VerifyPurchase or webhook integration for them.

The "Allow purchases without payload" compatibility setting

Go to App > Design > Core App Info > Advanced Settings and find "Allow purchases without requiring a developer payload or cached purchase context."

  • Enabled (the default for apps created before 5.8): caching is optional. If no context is found, brainCloud falls back to matching the product from the receipt's store product ID.

  • Disabled (the default for new apps): for googlePlay, itunes and metaHorizon, a purchase that has neither a developer payload nor a matching cached context fails verification. The error message is "Must use payload" and the result code is 101.

If you reuse store product IDs across multiple brainCloud products, we recommend updating your Google Play, Apple and Meta Horizon purchase flows to cache the payload context, and then turning this setting off.

Errors

Reason code

Cause

40700 INVALID_STORE_ID

The storeId isn't a valid brainCloud store ID, for example apple or oculus. Use itunes or metaHorizon.

40001 INVALID_REQUEST

The payload isn't a valid brainCloud payload string. Pass the value from GetSalesInventory() unchanged.

40358 MISSING_REQUIRED_PARAMETER

storeId, iapId or payload is missing or empty.

Examples

All examples assume you have already called GetSalesInventory() and have the selected product's store product ID and payload. They use the price-point approach above, where fruit_199 is shared by several items.

Raw API request (API Explorer)

Use service AppStore, operation CachePurchasePayloadContext:

{
"storeId": "googlePlay",
"iapId": "fruit_199",
"payload": "__bc:[1,apple_bundle,1,199,-1]"
}

Google Play (Unity)

// 1. Cache the brainCloud context right before purchasing
string storeId = "googlePlay";
string iapId = "fruit_199"; // Google Play productId
string payload = product["payload"].ToString(); // from GetSalesInventory

_bc.AppStoreService.CachePurchasePayloadContext(storeId, iapId, payload,
(response, cb) => {
// 2. Start the Google Play purchase
storeController.InitiatePurchase(iapId);
},
(status, code, error, cb) => Debug.LogError(error));

// 3. After Google Play reports success, verify immediately
var receipt = new Dictionary<string, object> {
{ "productId", iapId },
{ "orderId", purchase.orderId },
{ "token", purchase.purchaseToken }
};
_bc.AppStoreService.VerifyPurchase(storeId, JsonWriter.Serialize(receipt), OnVerifySuccess, OnVerifyFailure);

Apple App Store (Unity)

// 1. Cache the context; storeId for Apple is "itunes"
string storeId = "itunes";
string iapId = "fruit_199"; // App Store product_id
string payload = product["payload"].ToString();

_bc.AppStoreService.CachePurchasePayloadContext(storeId, iapId, payload,
(response, cb) => storeController.InitiatePurchase(iapId),
(status, code, error, cb) => Debug.LogError(error));

// 2. After StoreKit reports success, verify immediately.
// StoreKit 2 / App Store Server API: send the transaction ID
var receipt = new Dictionary<string, object> {
{ "transactionId", transactionId }
};
// Legacy receipts: { "receipt", base64AppReceipt }
_bc.AppStoreService.VerifyPurchase(storeId, JsonWriter.Serialize(receipt), OnVerifySuccess, OnVerifyFailure);

Meta Horizon / Quest (Unity)

// 1. Cache the context; iapId is the Meta Horizon SKU
string storeId = "metaHorizon";
string sku = "fruit_199";
string payload = product["payload"].ToString();

_bc.AppStoreService.CachePurchasePayloadContext(storeId, sku, payload,
(response, cb) => {
// 2. Launch the Meta Horizon checkout
IAP.LaunchCheckoutFlow(sku).OnComplete(OnCheckoutComplete);
},
(status, code, error, cb) => Debug.LogError(error));

// 3. On successful checkout, verify immediately
void OnCheckoutComplete(Message<Purchase> msg) {
if (msg.IsError) return;
var receipt = new Dictionary<string, object> {
{ "userId", metaUserId },
{ "sku", msg.Data.Sku },
{ "transactionId", msg.Data.ID }
};
_bc.AppStoreService.VerifyPurchase("metaHorizon", JsonWriter.Serialize(receipt), OnVerifySuccess, OnVerifyFailure);
}

Cloud Code

When purchases are routed through a client-session Cloud Code script:

var appStore = bridge.getAppStoreServiceProxy();
var result = appStore.cachePurchasePayloadContext(data.storeId, data.iapId, data.payload);

Note: this call isn't available from server-to-server (S2S) sessions, because the context is cached for the current user.

Best practices

  • Always use the payload from the latest GetSalesInventory() response. It reflects active promotions and price changes.

  • Make sure iapId exactly matches the product ID that will appear in the receipt. A mismatch means the cached context won't be found.

  • Cache once per purchase attempt. If the user cancels and buys again, cache again before the new attempt.

  • Keep the time between caching and VerifyPurchase short (60 seconds or less by default).

  • After every client build has been updated, disable "Allow purchases without requiring a developer payload or cached purchase context" to enforce correct product attribution.

Did this answer your question?