Coupons need iOS SDK 2.2.0 or Android SDK 1.4.0 or later, and a signed-in user
(identify). A guest gets notIdentified / NotIdentified before any request is made.
Coupons
Create coupon codes in the dashboard and let users redeem them in your app. The App Store and Google Play apply the discount; CashSDK records who used each code.
A coupon is a code you create in CashSDK, such as SPRING-25. It gives a percentage off, an
amount off, or a free period on the subscription plans you choose. The user types the code in
your app, and the store applies the discount:
| Platform | What CashSDK creates for each plan | How the user redeems it |
|---|---|---|
| iOS and macOS | An App Store offer code with a custom code | The SDK opens the App Store's offer code page with the code filled in |
| Android | A Google Play offer on the plan's base plan | The SDK buys the offer through Play Billing, like any purchase |
On the Mac, coupons work for universal purchase apps, whose products are App Store products. Coupons for products in a separate Mac App Store record are not supported yet. See macOS / Catalyst.
The discount is always charged by the store, never by unlocking access in the app. That keeps coupons inside App Store and Google Play payment rules.
Create a coupon#
CashSDK creates the store objects with your connected App Store Connect and Google Play credentials, so both must be connected for the app. Creating a coupon needs the admin role.
Dashboard, your app, Coupons, then create a coupon. Set the code, the discount, how many periods it lasts, the plans it covers, and optionally a start date, an expiry, a total cap and whether it is for new customers only. The form shows each store's limits before you save.
Each plan gets its own store offer. The coupon page shows each one's state. An App Store
code can take up to an hour before Apple accepts it; until then the SDK answers
notReady. Your app must be live on the App Store for iOS codes to redeem.
Store limits worth knowing before you create an iOS coupon: at most 25,000 uses, an expiry at most 6 months out, one use per Apple Account, and at most 10 active offer codes per plan at Apple. Apple also creates at least 500 codes per plan, so CashSDK enforces a smaller cap itself and switches the codes off when it is reached.
The same operations exist over the public API (/v1/coupons, with a live secret key) and in
the MCP server's tool list.
Redeem a coupon in your app#
Validate the code first to show the user what they get, then redeem it for one plan.
let check = try await CashSDK.shared.validateCoupon(codeField.text ?? "")
guard check.valid, let coupon = check.coupon, let plan = check.eligibleProductIds.first else {
showError(check.reason.map { CouponError.rejected($0).localizedDescription } ?? "")
return
}
showOffer(coupon) // kind, percentOff, formattedAmountOff(), duration, periodCount
let redemption = try await CashSDK.shared.redeemCoupon(coupon.code, productId: plan)
// The App Store is now open. When the user comes back:
switch try await CashSDK.shared.awaitCouponCompletion(redemptionId: redemption.redemptionId) {
case .completed(let entitlements): unlock(entitlements)
case .timedOut: break // a later purchase still arrives on entitlementUpdates
}
lifecycleScope.launch {
val check = CashSDK.shared.validateCoupon(codeField.text.toString())
if (!check.valid) return@launch showError(check.reason?.message ?: "This code cannot be used.")
val productId = check.eligibleProductIds.first()
// Several base plans under one coupon: let the user choose, then pass the choice.
val basePlanId = check.basePlanIds(productId).let { if (it.size > 1) choosePlan(it) else null }
try {
val result = CashSDK.shared.redeemCoupon(activity, check.coupon!!.code, productId, basePlanId)
if (result.alreadyOwned) showAlreadySubscribed() else unlock(result)
} catch (e: CouponException.OfferNotAvailableYet) {
showError(e.message) // a new Play offer can take a while to reach devices
} catch (e: CouponException.Rejected) {
showError(e.reason.message)
}
}
- A refused code is a result with
valid == falseand a reason (notFound,expired,exhausted,alreadyRedeemed,notEligible,notReadyand others), not an error. redeemCouponreserves one use first. The same user and coupon always get the same reservation, so a retry or a second tap is safe.- iOS: the purchase arrives through StoreKit and is verified like any purchase, even if the
app was killed while the user was in the App Store.
awaitCouponCompletionworks after a relaunch. - Android:
redeemCouponreturns the sameEntitlementsaspurchase. When the Google account already owns the plan, nothing is charged, the coupon is not used, andalreadyOwnedistrue. - Coupon offers never show up as trials. The Android SDK leaves them out of
products(), the paywall and everypurchasecall; onlyredeemCouponcan buy one.
The full API is in each SDK's README: iOS and Android.
Track redemptions#
The coupon page lists every redemption: who, which platform and plan, the price paid, the status and the exact time, with a CSV export. Sandbox and TestFlight purchases are labelled and never count toward a cap or a user's allowance.
Subscribe to the coupon.redeemed webhook to react in real time. It is
sent once per purchase chain, including for an iOS code typed straight into the App Store.