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).

  5. For each new transaction in the receipt, brainCloud works out which payload to use, in this order:

    1. A brainCloud payload carried by the store itself (for example a Google Play developer payload).

    2. A payload cached for the same app + user + storeId + iapId shortly before the time of the purchase (see Timing below).

    3. 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.

    4. No payload. The product is then matched using the receipt's store product ID.

  6. 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

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. 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

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 payload is found, brainCloud matches the product using the receipt's store product ID.

  • Disabled (the default for new apps): for googlePlay, itunes and metaHorizon, 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 101 and 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

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.

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 iapId you 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 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, 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 payloadSource in your VerifyPurchase responses while testing. It should be cache (or store for Google Play developer payloads), not cacheAtVerify or none.

  • 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?