Cancellation win-back with RTDN
A subscriber who cancels keeps access until the period ends. That gap is when a save offer can still change their mind, and the server is what knows the gap has opened.
Platform scope, up front
The cancellation save offer is implemented for Google Play, where it uses the native Play cancellation flow. It is not implemented for the Apple App Store: there is no StoreKit 2 equivalent wired, and the platform support matrix lists win-back as not implemented there. Everything below describes the Google Play path. See the platform support matrix for the current status of both stores.
Opting a package in
Win-back is configured per package. A package sets winbackOn: true and a discounted winbackPrice. Packages without winbackOn never show a save sheet. The discount is not a number you invent: winbackPrice is a real Play offer, so like price it is editable only on draft packages and becomes Play-owned once synced. Google still owns the transaction.
The offer itself can be created through the create_winback_offer MCP tool, against a product that is already mapped to an entitlement. The entitlement is derived from that mapping rather than typed in, and the tool follows the same preview-then-confirm pattern as every write tool: nothing is saved until a human approves the preview and the token is returned. A trigger of cancellation is the save-sheet case; targeted is the other supported value.
Step 1: the server learns about the cancellation
The app does not poll for cancellations. Google Play emits a SUBSCRIPTION_CANCELED Real-Time Developer Notification, and Tierux maps it to an entitlement effect: status cancelled, access left untouched because a cancelled subscription keeps access until expiry, and a win-back intent of set.
// src/services/rtdnProcessor.ts case 3: // SUBSCRIPTION_CANCELED — auto-renew off; access remains until expiry return { status: 'cancelled', winback: 'set' };
The intent does not blindly arm anything. A small resolver checks whether the product actually has a cancellation offer; if not, no flag is set and no sheet will appear.
// src/services/winback.ts if (intent === 'clear') return false; if (intent !== 'set') return undefined; const offer = await store.getWinbackOffer(appId, productId, 'cancellation'); return !!offer;
The result is stored on the entitlement as winbackPending. It is cleared when the subscription is recovered, renewed, purchased, or restarted, and again when it expires, so a stale prompt cannot outlive the moment it was relevant. The delivery guarantees underneath — idempotency, ordering, and retries — are the subject of chapter 4 of the verification guide, and they apply here unchanged.
Step 2: the app asks before it cancels
Your app wires its own "Cancel subscription" action to the SDK's win-back call. The SDK force-refreshes the entitlement and checks winbackPending. If it is not set, the call returns NotEligible without showing any UI, and your app sends the user on to Play's manage-subscriptions screen as usual. If it is set, the SDK presents a localized save sheet.
Tierux.showWinback(activity, paywallId, userId, entitlementId) { result -> when (result) { WinbackResult.Accepted -> // took the discounted offer; verified server-side WinbackResult.Declined -> // "cancel anyway" WinbackResult.NotEligible -> // no sheet shown; route to manage subscriptions is WinbackResult.Error -> // no sheet at full price is ever shown } }
What the save sheet shows
The sheet renders in the paywall's theme and accent. Top to bottom: the heading "Wait — before you go", a line offering the package at a special price, the regular price struck through (omitted if equal or blank) above the win-back price with its billing period, a primary button reading "Stay for" followed by the win-back price, the "Secured by Google Play" badge, and a "No thanks, cancel anyway" dismissal that lets the cancellation proceed. Every string is localizable through pw.localizations, inheriting the paywall's overrides.
On acceptance, the SDK selects the matching Play offer and runs the normal purchase, and the entitlement is refreshed through the ordinary verification path. If no matching offer can be found, the call ends in an error rather than showing a sheet at full price.
Limits worth knowing
- The flow is opt-in from your app. Tierux arms the flag and provides the sheet; your app decides to call it from its cancel action.
- iOS is not covered yet. The Swift SDK's shared purchase-result type compiles across platforms, but no App Store offer is redeemed through it.
- The price must be a real Play offer. There is no arbitrary discount value.
For a deeper look at how offer tokens are chosen and why this is a Google Play mechanism, read the win-back blog post.
Glossary: Win-back
The general definition of a win-back offer.
Glossary: RTDN
The Google Play notifications that arm the save sheet.
Blog: Win-back offers on Google Play
Candidate identification and offer-token selection in depth.
Docs: RTDN setup
Pointing Play Console at the shared Pub/Sub topic.
Feature: RTDN webhooks
Subscription lifecycle kept in sync without polling.
Platform support matrix
Win-back: Google Play implemented, Apple App Store not yet.
Save subscribers before they cancel
Free tier — unlimited apps, 1 paywall. No credit card, no revenue share.
Start free