Retention Messaging lets developers configure custom messages, images, and promotional offers that appear on the App Store cancellation page when a subscriber is about to cancel. It supports both static configuration via App Store Connect and real-time server-driven responses through the Retention Messaging API.
• Subscriptions using Retention Messaging see an average save rate increase of +1.4 points (82% lift), with promotional offer messages achieving up to +5.5 points (223% lift)
• Supports three display formats — message only, message + image, and message + offer — giving developers flexible ways to reduce churn at the most critical moment
• Real-time Retention Messaging lets servers respond per-customer with personalized messages, switch-plan offers, or promotional offers, with App Store Connect configuration as an automatic fallback
Demonstrates how to detect a redeemed retention offer (offerType 5) in a StoreKit transaction, and surface that information in a SwiftUI view so developers can validate their Retention Messaging integration.
import SwiftUI
import StoreKit
// MARK: - Model
struct SubscriptionStatus: Identifiable {
let id: UInt64
let productID: String
let offerType: Transaction.OfferType?
let offerID: String?
let expirationDate: Date?
}
// MARK: - ViewModel
@MainActor
final class RetentionViewModel: ObservableObject {
@Published var statuses: [SubscriptionStatus] = []
@Published var isLoading = false
func loadTransactions() async {
isLoading = true
defer { isLoading = false }
var results: [SubscriptionStatus] = []
for await verificationResult in Transaction.currentEntitlements {
guard case .verified(let transaction) = verificationResult else { continue }
guard transaction.productType == .autoRenewable else { continue }
let status = SubscriptionStatus(
id: transaction.id,
productID: transaction.productID,
offerType: transaction.offerType,
offerID: transaction.offerID,
expirationDate: transaction.expirationDate
)
results.append(status)
}
statuses = results
}
func isRetentionOffer(_ offerType: Transaction.OfferType?) -> Bool {
// offerType == 5 indicates a retention offer was redeemed
// (new in iOS 26.5 / iOS 27 alongside Retention Messaging)
guard let offerType else { return false }
return offerType == .promotional && offerType.rawValue == 5
}
}
// MARK: - View
struct RetentionMessagingDemoView: View {
@StateObject private var viewModel = RetentionViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
ProgressView("Loading transactions…")
} else if viewModel.statuses.isEmpty {
ContentUnavailableView(
"No Active Subscriptions",
systemImage: "creditcard",
description: Text("Purchase a subscription in sandbox to test Retention Messaging.")
)
} else {
List(viewModel.statuses) { status in
VStack(alignment: .leading, spacing: 6) {
Text(status.productID)
.font(.headline)
if let offerType = status.offerType {
Label(
offerType.rawValue == 5
? "Retention Offer Redeemed (type 5)"
: "Offer type \(offerType.rawValue)",
systemImage: offerType.rawValue == 5
? "checkmark.seal.fill"
: "tag"
)
.foregroundStyle(offerType.rawValue == 5 ? .green : .secondary)
.font(.subheadline)
} else {
Label("No offer redeemed", systemImage: "minus.circle")
.foregroundStyle(.secondary)
.font(.subheadline)
}
if let offerID = status.offerID {
Text("Offer ID: \(offerID)")
.font(.caption)
.foregroundStyle(.secondary)
}
if let expiry = status.expirationDate {
Text("Expires: \(expiry.formatted(date: .abbreviated, time: .shortened))")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 4)
}
}
}
.navigationTitle("Retention Messaging")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Refresh") {
Task { await viewModel.loadTransactions() }
}
}
}
.task {
await viewModel.loadTransactions()
}
}
}
}
#Preview {
RetentionMessagingDemoView()
}iOS 27 enables developers to sell auto-renewable subscriptions to multiple people at once via in-app group purchases or volume purchasing through Apple Business/School Manager. Purchasers buy a set number of seats and share an invite link; StoreKit 2 handles the full seat-assignment lifecycle.
iOS 27 introduces a Product Page Header on the App Store — a dedicated visual area above screenshots where developers can place custom marketing images or videos. A new Asset Library in App Store Connect centralizes all creative assets and allows real-time updates to Product Page Headers and Search Result visuals without a new app submission.
• A new offerType value of 5 indicates a retention offer was redeemed — update any server-side transaction parsing logic accordingly • Real-time endpoint must respond quickly; if it times out the App Store falls back to App Store Connect config, then to default API messages • When an offer is shown, it replaces the image in the UI — you cannot show both simultaneously • Monthly subscriptions with 12-month commitment (introduced in iOS 26.5) require the billingPlanType field in alternateProduct responses
Requires auto-renewable subscription setup in App Store Connect; real-time Retention Messaging requires passing a sandbox performance test before production enablement