Implementing Offer Codes for Subscriptions in Mobile Applications
Offer Codes are Apple promo codes for subscriptions. They allow you to provide free access or a discount without binding to an advertising network, without requiring credit card information. Typical scenarios: partner promotions, codes for influencers, offline distribution (QR on packaging), corporate sales.
The principal difference from Promotional Offers: Offer Code doesn't require server-side signature and can be activated through a system dialog directly in the app or via a link https://apps.apple.com/redeem?ctx=offercodes&id=...&code=....
Creating Codes in App Store Connect
Subscriptions → [Subscription] → Offer Codes → +. Set Reference Name, Offer ID, discount type (freeTrial / payAsYouGo / payUpFront), duration. Apple allows creating single-use codes (One-Time Use) or reusable (Custom). The limit for single-use codes is 150,000 per quarter per subscription.
Implementing Redemption Dialog in the App
StoreKit 2 provides OfferCodeRedeemSheet — a system UI for code entry:
import StoreKit
import SwiftUI
struct SettingsView: View {
@State private var showRedeemSheet = false
var body: some View {
List {
Button("Enter Promo Code") {
showRedeemSheet = true
}
}
.offerCodeRedemption(isPresented: $showRedeemSheet) { result in
switch result {
case .success(let transaction):
// User activated the code — update UI
await updateSubscriptionStatus(transaction)
case .failure(let error):
handleRedemptionError(error)
case .pending:
// Transaction is being processed
break
}
}
}
}
On UIKit (iOS 16+) the analog via AppStore.presentOfferCodeRedeemSheet(in:):
Task {
do {
try await AppStore.presentOfferCodeRedeemSheet(in: windowScene)
} catch {
// Show fallback — link to apps.apple.com/redeem
}
}
On iOS below 16 presentOfferCodeRedeemSheet is unavailable. Fallback: open URL https://apps.apple.com/redeem?ctx=offercodes&id=APP_ID&code=CODE via UIApplication.open.
Handling Transactions After Activation
After successful code activation, StoreKit generates a transaction. You need to catch it via Transaction.updates:
// Starts at app launch and listens all the time
for await result in Transaction.updates {
if case .verified(let transaction) = result {
if transaction.offerType == .code {
// Offer Code activated — unlock premium content
await unlockPremiumContent()
await transaction.finish()
}
}
}
Important to call transaction.finish() — without it, the transaction remains in the pending queue and can appear again at the next launch.
Android — Google Play Promo Codes
On Android the analog is Promo Codes in Google Play. Differs in architecture: codes are created in Google Play Console, activated via Play Store or deeplink. On the app side via Billing Library 5+:
// Google Play notifies through PurchasesUpdatedListener
override fun onPurchasesUpdated(result: BillingResult, purchases: List<Purchase>?) {
if (result.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
handleNewPurchase(purchase)
}
}
}
}
Common Issues
Single-use codes don't have Preview in Sandbox until they're actually released — testing requires creating a Custom code or sandbox account with manually applied code via Apple's test link.
Another issue: if the app doesn't listen to Transaction.updates on cold start, and the user activated the code via web link — the transaction waits in the queue, but the app won't unlock content until the next explicit call to Transaction.currentEntitlements.
What's Included in the Work
- Setting up Offer Code in App Store Connect
- Integration of
OfferCodeRedeemSheet/AppStore.presentOfferCodeRedeemSheet - Fallback for iOS below 16
- Handling transactions via
Transaction.updates - Testing with Sandbox Offer Codes
Timeline
3–5 days considering integration into existing paywall and subscription flow. Pricing is calculated individually.







