The store owns the money; CashSDK owns the targeting and the record. You configure the discount itself in App Store Connect or the Play Console, because that is what actually charges the customer. You then describe it in CashSDK so it knows who to present it to, and so every redemption is attributed to a customer instead of arriving as an unexplained cheap transaction.
Offers and promo codes
Set up introductory, promotional, win-back and offer-code pricing, target who sees each one, and track every redemption back to the customer.
CashSDK models four kinds of discounted pricing, matching what Apple and Google let you sell:
| Kind | What it is | Who gets it |
|---|---|---|
intro | The first-time price on a subscription, including a free trial | Anyone who has never bought in the group |
promotional | A discount you present to a current or lapsed subscriber | Whoever you target |
win_back | A discount aimed at customers whose subscription ended | Whoever you target |
offer_code | A redeemable code: App Store offer codes, Play promo codes | Whoever you give the code to |
Want a code you create once in CashSDK and users type in your app? See Coupons: CashSDK creates the App Store offer codes and Play offers for you.
Set up an offer code#
App Store Connect: your subscription, Subscription Pricing, Offer Codes. Create a code group, set the discount and how many redemptions it allows, then download the one-time-use codes or copy the custom code.
Play Console: Monetize, Promo codes. Create a promotion for the product, choose one-time or custom, and generate the codes.
Dashboard, your app, Offerings, Offers, Create offer. Pick the
product, set the kind to Offer code, and put the store's own offer id in
identifier. That id is the join key: it is what the store stamps on the
transaction, and how a redemption finds its way back to this row.
Or over the API:
curl -X POST https://api.cashsdk.com/v1/apps/$APP_ID/catalog/offers \
-H "Authorization: Bearer $CASHSDK_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"productId": "prod_...",
"kind": "offer_code",
"identifier": "LAUNCH50",
"displayName": "Launch week, 50% off",
"paymentMode": "pay_as_you_go",
"discountPct": 50,
"duration": "P1M",
"periodCount": 3
}'
Offer codes are redeemed at the store, not in your app: Apple opens a redemption
sheet (or apps.apple.com/redeem), Play takes the code at checkout. Nothing has
to change in your app for a code to work.
Use the store's id verbatim. An offer whose identifier does not match what the
store stamps still works for the customer, but its redemptions arrive
unmatched: recorded, attributed to the customer, and not linked to your row.
Target a promotional or win-back offer#
Promotional and win-back offers carry an audience, and CashSDK evaluates it
with the same matcher the device path runs. Before you save one, the dashboard
tells you how many real customers it currently targets:
curl -X POST "https://api.cashsdk.com/v1/apps/$APP_ID/catalog/offers:preview-audience" \
-H "Authorization: Bearer $CASHSDK_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"audience": {"conditions": [{"key": "expired_within_days", "value": [0, 30]}]}}'
The count is computed over real customer rows, so it is the number that would
actually be offered, not an estimate. exact: false means the scan hit its
bound and the number is a floor.
Apple requires a signature on a promotional offer before StoreKit will accept it. Once an In-App Purchase signing key is provisioned, ask for one per customer:
curl -X POST https://api.cashsdk.com/v1/apps/$APP_ID/catalog/offers/$OFFER_ID/signature \
-H "Authorization: Bearer $CASHSDK_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"appUserId": "user_123"}'
Without a key it answers { "signed": false } and the reason. It never returns a
fabricated signature, because a bad one fails at the payment sheet.
Track redemptions#
Every redemption is written by the money path when the store reports the transaction, so the numbers are records rather than estimates.
In the dashboard. Offerings shows each offer with how many times it has been
redeemed, and a feed of individual redemptions: the offer type, the product, the
store's offer id, which customer redeemed it, what they paid, and when. A
redemption the store reported with no app user attached reads unattributed
rather than guessing.
Over the API.
curl "https://api.cashsdk.com/v1/apps/$APP_ID/catalog/offers/redemptions?offerId=$OFFER_ID&limit=100" \
-H "Authorization: Bearer $CASHSDK_SECRET_KEY"
Each row carries userId, productIdentifier, offerIdentifier, offerType,
priceMinor, currency, environment, restoredEntitlement and redeemedAt.
As a webhook. Subscribe to offer.redeemed to react in real time. Its payload
carries userId, offerId, offerType, offerIdentifier, productIdentifier,
transactionId, restoredEntitlement, priceMinor and currency.
restoredEntitlement is the one to watch: it means a churned customer just
came back, which is the whole point of a win-back.
Redemptions outlive the offer row. Delete an offer and its redemptions stay, so a finished campaign keeps its history.
What a redemption rate can and cannot be#
For a targeted offer, the denominator is the audience: redemptions against
the customers the offer targets. Read the count from offers:preview-audience
and the redemptions from the endpoint above.
For an offer code, there is no honest denominator in CashSDK. The store issues the codes and does not report how many were handed out, so CashSDK can tell you exactly who redeemed and when, but not what share of your codes were used. App Store Connect and the Play Console report issuance on their side.