Automated Promotions with no discount — "Not for Sale" workaround and purchase count tracking
-
Hi BrainCloud team,
We're implementing Automated Promotions to gate specific products behind user segments and time windows — but without any price discount. The goal is simply to make a product available (at its regular App Store price) for a limited time to eligible users, not to offer it at a reduced price.
The console constraint we ran into:
When setting up a Cash Product in an Automated Promotion, the dashboard enforces that the promotional price must differ from the default price. Since we don't intend to offer a discount, we can't configure a valid second price tier.
Our current workaround:
We set the promotional price to "Not for Sale" (priceId: -2). This lets us pass validation and receive the itemId in the GetEligiblePromotions response, which we use to look up the product. The actual purchase goes through the App Store at the product's standard list price — our client ignores the priceId and referencePrice entirely.
Our question:
With this "Not for Sale" workaround, does the Promotion's purchased counter increment correctly when a successful IAP purchase is verified via verifyPurchase?
We're relying on purchased >= maxPurchases to prevent the promotion from re-appearing after a user has already bought the product. If the counter doesn't increment in this setup, we need to handle purchase tracking ourselves.
Feature request:
Is there a plan to support promotions that sell products at their default/list price without requiring a discounted price tier? For use cases like timed exclusives or segment-gated access (rather than sales), this would remove the need for workarounds.
Thanks in advance!
-
If your promotion is for a cash product and the promotional product is set to
Not for Sale, you need to complete the flow by callingverifyPurchasein brainCloud with thepurchasePayloadfrom the store.As long as that payload contains the
promotionIdyou received fromGetEligiblePromotions, the promotion will be correctly attributed and the server-side flow will reach the counter increment logic.Also make sure you set
maxPurchaseson the promotion configuration page so the system knows when to stop returning that promotion for the user.As for your feature request to allow promotions without requiring a discount, I will forward this to our team for review.
-
Hi - I'm probably missing the subtleties of your use case.
But in your scenario, what folks would normally do is:
- Set the product as NOT FOR SALE by default.
- Then have Promotion set it as AVAILABLE at the STANDARD price point.
Is there a reason that you don't set the product as NOT FOR SALE initially?
-
Thank you for the responses. I'd like to clarify my situation and ask more specific follow-up questions.
My current setup:
- Product default price: Standard price (normally available for purchase)
- Promotion price override: Not for Sale (priceId: -2)
- Reason: The console requires the promotion price to differ from the default price, so this is used as a workaround to pass validation
- Actual purchase price: Provided directly by Apple App Store / Google Play
In other words, I'm using promotions not to change the price, but to control access for specific user segments and track purchase counts.
Question 1: Is using "Not for Sale" as a promotion price override a supported pattern?
With the setup described above — where the promotion price override is set to Not for Sale (priceId: -2) — does BrainCloud's purchase counter and maxPurchases limit work correctly? Is this a supported use case?
Question 2: How exactly should promotionId be passed during purchase?
JasonL mentioned:
"As long as that payload contains the promotionId you received from GetEligiblePromotions, the promotion will be correctly attributed and the server-side flow will reach the counter increment logic."
However, in a standard IAP flow:
- Call GetEligiblePromotions → receive promotionId
- User completes payment on Apple App Store / Google Play → store returns a receipt (binary/base64 data generated by Apple/Google)
- Call AppStoreVerifyPurchase (or equivalent) with that receipt
The receipt from step 2 is generated by Apple/Google — I cannot inject custom fields like promotionId into it.
So my question is: in which parameter and in what format should promotionId be passed in the verifyPurchase call? A concrete example of the API call with promotionId correctly included would be extremely helpful.
Summary:
- Question
- Is setting the promotion price override to Not for Sale (priceId: -2) a supported pattern, and will the purchase counter work correctly?
- In the verifyPurchase API call, which parameter carries the promotionId, and in what format?
-
Following up with an additional question related to this thread:
We're testing the promotion and purchase flow in the iOS Sandbox environment, and we're not seeing the Purchase Count increment after a successful sandbox purchase — even when calling verifyPurchase with the promotionId included.
Is purchase count tracking supported in the iOS Sandbox environment, or is it only tracked for real (production) purchases?
If sandbox purchases are not counted, is there a recommended way to test the maxPurchases limit before going to production?
Any guidance would be appreciated. Thank you!
-
Question 1
Let me clarify how this is normally handled.
The client app gets a list of the available items for purchase by calling GetSalesInventory(). That call take into account who the user is, what segments they are in, etc. to determine which Cash Products to return in that call.
So if you want to use segments to determine who can purchase an item, you would normally:
- Set the item as "Not for Sale" as it's default price
- Then in the segmented Promotions, you set the sale (in this case "Regular") price.
In that way - people normally cannot buy the product - but it the user is in the segment covered by the promotion - it WILL appear at what you call the "regular price" for that user when they call GetSalesInventory().
Do you get how this does what you are asking for?
Question 2 - How is promotionId to be passed.
The promotion is automatically taken into account when calling GetSalesInventory(). The trick is to still know a promotion is involved when we are processing the purchase receipt. That is accomplished by caching the new "payload" field.
So the client should:
- Call GetSalesInventory() to determine what products to show to the user
- Bring up their store UI so the user can view things. Probably involves calling the platform's store APIs to get localized prices of everything.
- When the user selects an item to purchase, the client app world:
- Call AppStore.CachePurchasePayloadContext() with the "payload" field that was associated with the item the user has chosen.
- then call the platform's appstore api to initiate the purchase.
Then when brainCloud is processing the purchase it will retrieve the cached "payload" information - which includes information about the promotionId that was involved (if there was one).
So note - in this flow you don't really NEED to call GetEligiblePromotions() - though that can be handy if you need to use that to show some banners in your store or something.
Regarding counting of sandbox purchases - I don't think we include those in the stats. We record the purchase so that you can verify that it is happening end-to-end, but we now keep those totals out of the stats.
I hope that helps!
-
Thank you for the responses. Based on the discussion, we've implemented the following:
- Product default price: standard App Store price
- Promotion price override: "Not for Sale" (priceId: -2) — workaround to pass console validation requiring a price difference
- Purchase verification: server-to-server (S2S)
VERIFY_PURCHASE, no explicitpromotionIdin the request - Promotion gating: via
GetEligiblePromotions()on the client
We're observing that the
purchasedcounter increments correctly after purchase, and the promotion does not reappear after app restart.We believe the reason automatic attribution works without calling
CachePurchasePayloadContext()is that we set Apple'sappAccountToken(a UUID embedded in the StoreKit 2 signed transaction) to the brainCloud profileId before initiating the native IAP purchase. When brainCloud receives the JWS receipt during S2SVERIFY_PURCHASE, it can extract theappAccountToken, identify the user, look up their active promotions, and increment the counter automatically.Two quick questions:
- Is this understanding correct — does brainCloud use the
appAccountTokenin the StoreKit 2 JWS to perform automatic promotion attribution, makingCachePurchasePayloadContext()unnecessary in this flow? - When we extend this to Android (Google Play), there is no equivalent of
appAccountTokenin the receipt. What is the recommended approach to ensure promotion attribution works correctly in that case — should we callCachePurchasePayloadContext()before the purchase, or is there another mechanism?