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
The client calls GetSalesInventory(). Each product returned includes a
payloadstring, for example__bc:[1,apple_bundle,1,199,-1].When the user taps Buy, the client calls CachePurchasePayloadContext(storeId, iapId, payload) with that product's payload right before starting the native store purchase.
The client completes the purchase with the store (Google Play Billing, StoreKit or the Meta Horizon Platform SDK).
The client calls VerifyPurchase(storeId, receiptData).
For each new transaction in the receipt, brainCloud works out which payload to use, in this order:
A brainCloud payload carried by the store itself (for example a Google Play developer payload).
A payload cached for the same app + user + storeId + iapId shortly before the time of the purchase (see Timing below).
A payload cached for the same app, user, store and product ID shortly before VerifyPurchase was called. This is a fallback for clients that cache late.
No payload. The product is then matched using the receipt's store product ID.
brainCloud checks that the payload makes sense for the purchase, then uses it to determine the product, price and promotion to award.
Each cached payload is used for one purchase only. It is deleted once that purchase has been recorded.
Purchases that brainCloud has already recorded (for example, a purchase that is verified again or restored) never use the cache, so a payload you cache for the player's next purchase is not taken by an old one.
Parameters
Parameter | Description |
| The brainCloud store ID: |
| The store's own product ID. It must exactly match the ID in the receipt: Google Play |
| The |
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. The optional sixth value, campaignScenarioCode, was added in brainCloud 6.0 and is present when a LiveOps campaign scenario applies to the product.
Timing
brainCloud matches a cached payload to a purchase using the purchase time from the store receipt. By default, a payload matches if it was cached up to 5 minutes before the purchase (or up to 5 seconds after it, to allow for clock differences). If several payloads match, the most recent one cached before the purchase is used.
Because matching is based on the purchase time, VerifyPurchase does not have to be called immediately. A purchase that is verified later still finds its payload, for example after a crash, on the next app launch, after a pending or "Ask to Buy" payment completes, or when a Meta Horizon purchase is found later through GetViewerPurchases. Cached payloads are kept for 30 days by default.
If no payload was cached around the purchase time, brainCloud falls back to a payload cached up to 60 seconds before VerifyPurchase is called. This supports clients that cache the payload late, but it is less reliable, and brainCloud logs a warning when it happens (see Troubleshooting). For best results:
Call CachePurchasePayloadContext immediately before starting the store purchase, not when the shop screen opens and not after the purchase.
Call VerifyPurchase when the store reports a successful purchase. If that's not possible right away, verify as soon as you can.
How brainCloud checks the payload
For new purchases on Google Play, Apple and Meta Horizon, brainCloud checks the payload against the purchase before using it:
If the payload names a brainCloud product that is not sold under the purchased store product ID, the payload is ignored and the purchase is handled as one without a payload. This protects against tampered clients and against payloads cached under the wrong
iapId.If a cached payload names a product that no longer exists, the payload is ignored and the purchase is handled as one without a payload.
If the payload names a product that has been archived, the purchase is still credited to that product.
The price recorded for the purchase is the product's real price that matches the purchase (the default price is preferred), not the reference price in the payload.
An ignored payload does not cause an error by itself. The transaction's payloadSource is reported as none, and a warning is written to your app's logs.
Supported stores
Store | storeId | Uses cached context? | Notes |
Apple App Store |
| ✅ Yes | Apple receipts never contain a developer payload, so the cached context is always used. |
Google Play |
| ✅ Yes | If the receipt includes a brainCloud |
Meta Horizon (Quest) |
| ✅ 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 payload is found, brainCloud matches the product using the receipt's store product ID.
Disabled (the default for new apps): for
googlePlay,itunesandmetaHorizon, a new purchase without a payload is handled based on its store product ID:If the store product ID is used by exactly one brainCloud cash product, the purchase is processed and credited to that product.
If it is used by more than one cash product, brainCloud can't tell which was bought, so the transaction is not processed. Its result code is
101and the error message starts with "Must use payload - store product ID ... is used by more than one cash product".If it isn't used by any cash product, the transaction fails with "Product not found" (result code
102).
Verifying a purchase that brainCloud has already recorded never requires a payload, whichever way this setting is set.
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
CachePurchasePayloadContext can return these errors:
Reason code | Cause |
| The |
| The |
|
|
Troubleshooting
Checking which payload was used
Each transaction in the VerifyPurchase response includes a payloadSource field:
store: the payload came from the store's own purchase data.cache: a payload cached around the purchase time was used. This is the expected value.cacheAtVerify: no payload was cached around the purchase time, so one cached just before VerifyPurchase was used. Your client is caching the payload too late.none: no payload was used, either because none was found or because the payload was ignored.
Log messages
These messages can appear in your app's logs (App > Global > Logs):
"Verify window payload used - purchase window payload is missing...": the payload was cached after the purchase, at VerifyPurchase. Move your CachePurchasePayloadContext call to just before the store purchase starts.
"...so the payload was ignored and the purchase was handled as one without a payload.": the payload named a product that doesn't exist, or a product that isn't sold under the purchased store product ID. Check that the
iapIdyou cache matches the product being bought. If it does, this can indicate a tampered client."...was not processed. Store product ID ... is used by ... cash products...": a purchase arrived without a payload while the compatibility setting is off, and its store product ID is shared by several products. Make sure the client caches the payload, or give each product its own store product ID.
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 the purchase
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 the purchase.
// 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 the purchase
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);
}
Purchases made outside your game, for example directly from your add-on's Meta Horizon store page, have no cached payload. When you find them later (for example through GetViewerPurchases) and verify them, they are matched by SKU. With the compatibility setting off, this works as long as the SKU is used by only one cash product.
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
payloadfrom the latest GetSalesInventory() response. It reflects active promotions and price changes.Make sure
iapIdexactly matches the product ID that will appear in the receipt. A mismatch means the cached context won't be found, or will be ignored.Cache once per purchase attempt, immediately before starting the store purchase. If the user cancels and buys again, cache again before the new attempt.
Check
payloadSourcein your VerifyPurchase responses while testing. It should becache(orstorefor Google Play developer payloads), notcacheAtVerifyornone.After every client build has been updated, disable "Allow purchases without requiring a developer payload or cached purchase context" to enforce correct product attribution.
