◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-06-12Advanced

Your App Won't Notice the Refund — Revoking Entitlements with REFUND Notifications and the Voided Purchases API

A refund doesn't reach your app on its own — a cached premium flag survives it. Implementation notes on revoking access via App Store Server Notifications V2, Google Play's Voided Purchases API, and RevenueCat.

RevenueCat30App Store93Google Play44subscriptions10in-app purchases

✦ Premium Article

I was going through the monthly report for one of my wallpaper apps when I noticed a few small negative rows in the sales list. App Store refunds. The amounts were trivial — what bothered me was something else entirely. I realized the refunded users' devices were almost certainly still running with the lifetime Premium unlock fully active.

I checked, and I was right. At the time, my app set a local "premium" flag on a successful purchase and simply read that flag on launch. When a refund goes through, neither Apple nor Google reaches out to your app on its own. Unless you receive the server notification and revoke the entitlement yourself, the refunded user keeps the feature indefinitely.

For an indie developer, refunds are a handful of cases per month at most, so this is easy to postpone. But the higher your one-time or subscription price, the harder it becomes to ignore. These are my notes from wiring refund detection into my own apps, including the parts where I stumbled.

Why refunds never reach your app

An App Store refund is settled entirely between the user and Apple. The user files a request through "Report a Problem," Apple approves it, and the refund is done — nothing is pushed to your app at that moment. StoreKit 2 does record a revocationDate on the affected transaction, but that's information your app only sees if it actively reconciles transactions.

Google Play behaves the same way. If a refund issued through the Play Console or the API includes "revoke entitlement," the purchase is voided — but the client only finds out the next time the Billing Library reconciles purchase state.

The trouble is that many apps, mine included at the time, treat purchase handling as "set a local flag on success and move on." Under that design, the flag stays up after the refund, and the user keeps Premium. Across my wallpaper apps, refunds sit around 0.5% of sales — small, but the highest-priced lifetime tiers are exactly where each case hurts most, and left alone it quietly becomes a known loophole: refund first, keep using it anyway.

Decide the path to revocation first — three architectures

Before writing any code, decide which route refund information takes into your system. There are effectively three options.

Option A: centralize on RevenueCat

Pairing a Rork-generated app with RevenueCat is the standard setup for in-app purchases, and it's also a strong choice for refund detection. RevenueCat receives server notifications from both Apple and Google, and when it detects a refund it expires the entitlement automatically. Your app's only job is to reconcile CustomerInfo.

Option B: receive server notifications yourself

If you run your own subscription backend, consume App Store Server Notifications V2 and Google Play's Real-time Developer Notifications (RTDN) directly. It's more work, but you also get first-party access to everything else — billing retries, cancellation intent, offer redemptions.

Option C: client-side reconciliation only

For a small app with no backend, you can reconcile StoreKit 2's currentEntitlements (queryPurchasesAsync on Android) at launch and drop revoked transactions. Detection slips to "next launch," but that's still a major improvement over an unattended local flag.

Since most of my apps already route purchases through RevenueCat, I default to Option A and add Option B only where I need purchases tied to my own user IDs.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦A Node.js webhook that verifies App Store Server Notifications V2 signatures and revokes access on REFUND, using @apple/app-store-server-library
✦How refunds propagate through RevenueCat, and how to stop a cached premium flag from outliving the refund
✦Android-side revocation with SUBSCRIPTION_REVOKED and the Voided Purchases API, plus practices that keep my refund rate around 0.5%
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-08-20
A beta-SDK build can reach TestFlight, but it can't reach review
Builds made with a beta Xcode can be distributed through TestFlight, but they cannot be submitted for App Store review. Here is how to check which SDK produced your build, and how to protect your release profile in eas.json.
⬡ Dev Tools2026-08-19
Three conditions that make Play Policy Insights report nothing on an Expo project
Google Play now ships an open-source policy auditing skill. Running it against an Expo-shaped project, one directory argument moved the result from five detected data categories to zero. Here is when the scan actually reaches your code, and where its output should not be trusted.
⬡ Dev Tools2026-08-18
Version code 1 has already been used — who owns that number, app.json or EAS?
When a Play upload stops on versionCode, check where the number actually lives before bumping it. A one-command way to tell whether app.json, EAS, or build.gradle wins.