StoreKit 2 refund detection comes down to one property on the transaction, and it's revocationDate
When Apple refunds one of your customers, the refund is already sitting inside your app on the transaction's revocationDate, before your server job runs. Here is where StoreKit 2 refund detection surfaces on the device, what revocationDate and revocationReason tell you, and why the client is for speed and the server is for truth.

Key takeaways
- In StoreKit 2 a refunded purchase carries a non-nil revocationDate on its Transaction, so your app can detect the refund on its own, without calling your server.
- revocationDate is set when the App Store refunds a transaction or when a customer loses it through Family Sharing, so a non-nil date is not always a refund.
- revocationReason tells you why: developerIssue means the customer cited a problem in your app, and other covers every remaining refund reason.
- Transaction.currentEntitlements already excludes refunded and revoked purchases, so the cleanest client-side gate for access is simply whether a product still appears there.
- Transaction.updates only delivers a refund that happened while your app was closed if you start listening at launch, so a missing Task means a missed refund.
- Client-side detection only fires while the app is open, which is why App Store Server Notifications V2 REFUND stays the authoritative signal that stops you paying to serve a refunded user.
- A refund can be reversed, and when it is, the revocation fields are removed from the transaction and you are expected to restore the access you cut.
Most apps learn about an Apple refund from their server, through an App Store Server Notification, and never notice that the same refund is already sitting inside the app. It is on the transaction, in a property called revocationDate, and reading it lets your app cut a refunded customer's access the next time they open it, instead of waiting on a backend job. StoreKit 2 refund detection is a client-side signal most teams skip. Here is exactly where a refund surfaces on the device, what it tells you, what it does not, and why it belongs next to your server notifications rather than in place of them.
Where a refund shows up inside StoreKit 2
StoreKit 2 hands you transactions as signed values, and a refund does not delete the transaction. It marks it. Two properties on the Transaction carry the mark, and both stay nil for the entire life of a healthy purchase. When one of them turns non-nil, the App Store has taken the purchase back.
revocationDate is the field that flips
revocationDate is an optional Date. Apple's own description is exact: it is the date the App Store refunded the transaction or revoked it from Family Sharing. For a purchase that is still good, it is nil. The moment a refund is processed, it holds the timestamp of that refund. That single check, is revocationDate non-nil, is the whole of client-side refund detection. Everything else is nuance stacked on top of it.
revocationReason tells you why Apple pulled it
revocationReason sits next to the date and explains the cause. StoreKit gives it two values that matter for refunds. developerIssue means the customer told Apple the refund was due to an actual or perceived problem in your app. other covers every remaining reason. A third value, upgradedToBundle, is not a refund at all; it flags a transaction the App Store revoked because the customer moved to a subscription bundle. Read the reason before you act, because developerIssue is the one worth counting: a cluster of them is your own product telling you where it broke.
| Property | Type | What a non-nil value means |
|---|---|---|
revocationDate | Date? | The App Store refunded this transaction, or revoked it through Family Sharing, on this date |
revocationReason is developerIssue | reason | The customer cited an actual or perceived problem in your app |
revocationReason is other | reason | The refund happened for some other reason Apple does not itemize |
revocationReason is upgradedToBundle | reason | Not a refund; the transaction was revoked because the customer switched to a subscription bundle |
currentEntitlements already drops a refunded purchase
You do not always have to read the revocation fields yourself. Transaction.currentEntitlements is the sequence of purchases a customer is still entitled to right now, and Apple builds it to leave out the ones you should not honor. A product the App Store has refunded or revoked does not appear in it. Neither do expired subscriptions, or consumables, which are gone the moment they are finished.
That makes currentEntitlements the cleanest gate for access. Ask it what the customer owns, grant exactly that, and a refund removes the entitlement for you without a single revocationDate check. The revocation fields are for when you want the detail, the date and the reason, to log the event or to react to it. The entitlement list is for the plain question of whether to keep the lights on.
StoreKit 2 refund detection in practice, from launch and while the app runs
There are two moments your app can catch a refund on the device, and they need different code. One is while the app is open and a refund happens live or on another device. The other is at launch, catching up on everything that changed while you were closed. Miss the second and your StoreKit 2 refund detection has a hole exactly where most refunds fall, because customers rarely have your app open when they ask for one.
Start listening at launch or you miss the refunds that happened while closed
Transaction.updates is the async sequence that emits a transaction whenever the system creates or updates one outside your app or on another device, a refund included. Apple's instruction is blunt: start a Task that iterates it as soon as your app launches, or you may miss the transactions it delivers only once at startup. A refund that landed overnight arrives through updates the next time the app opens, but only if a listener is already running to receive it. No listener, no event, and the refund stays invisible until something else reconciles it.
A purchase on the same device does not come through updates
One trap catches people who test refunds by hand. A normal purchase made on the same device does not arrive through updates; StoreKit returns it straight from the purchase call's result. updates is for the out-of-band changes: refunds, Ask to Buy approvals, offer-code redemptions, and purchases made elsewhere. So build your refund handling around updates and currentEntitlements, not around the purchase flow, because the refund will never walk back through the path the sale took.

What client-side detection cannot do for you
Reading refunds on the device is fast and it is free, but it has a ceiling, and pretending it does not is how revenue leaks. The device only knows what StoreKit has told it, and StoreKit only speaks while your app is running. A customer who gets a refund and never opens your app again is a customer your client-side check never sees.
A revocationDate is not always a refund
The same field flips for Family Sharing. When a customer loses access to a shared purchase, because the organizer removed them or the sharing ended, that transaction gets a revocationDate too. So a non-nil date means the customer no longer has this purchase, which is exactly what you need for access control, but it does not always mean money came back out. If you are counting refunds for revenue, separate the Family Sharing revocations from the real ones before you trust the number.
A refund can be reversed
A refund is not always final. Apple can reverse one, and when it does, the revocation fields are removed from the transaction and the purchase is valid again. If you cut access on the refund, you are expected to restore it on the reversal. On the device that shows up as another updates event with a clean transaction; on your server it is a distinct REFUND_REVERSED notification. Handle only the refund and you will strand a paying customer with no access and a working receipt.
What a late revoke actually costs
A refund is rarely just the sale price leaving your account. By the time it clears you have usually already spent real money serving that purchase, and that spend does not come back. The generated images cost GPU minutes. The chat answers cost model API calls you were billed for per token. The uploads cost storage you are still paying to hold. If the purchase funded a payout to a creator, that money is already out the door. None of it reverses with the refund.
Client-side detection shrinks the window on the one part you can still control, which is future spend. The sooner you know a purchase is refunded, the sooner you stop serving it. But the device only tells you while the app is open, so a refunded user who never comes back keeps whatever server-side access you granted, quietly costing you every time a background job or a synced device acts on their behalf. The client makes the revoke fast. It does not make it guaranteed.
Use the client for speed and the server for truth
The clean design uses both signals for what each is good at. On the device, Transaction.updates and currentEntitlements give you an instant, local reaction the moment a refunded customer opens the app, good for the UI and for finishing entitlement changes without a round trip. On the server, App Store Server Notifications V2 send a REFUND message that arrives whether or not the app is ever opened again, which is the only signal that reliably stops your backend from spending on a refunded account.
| Signal | Where it lives | Fires when | Trust it to |
|---|---|---|---|
revocationDate on a transaction | Device, StoreKit 2 | Your app reads the transaction | Tell you a specific purchase was refunded or revoked |
Transaction.updates | Device, StoreKit 2 | A refund lands while the app runs, or at launch if you listen | React instantly for a customer who is present |
currentEntitlements | Device, StoreKit 2 | You check what the customer owns now | Gate access without tracking refunds yourself |
REFUND notification | Your server, App Store Server Notifications V2 | Apple processes the refund, app open or not | Stop server-side spend on a customer who never returns |
Wire the device signals for the customer who is holding the phone, and the server notification for the one who is not. The refund shows up in both places on purpose. Reading only one of them is how a refunded account keeps costing you after the sale is already gone.
Frequently asked questions
- How do I detect a refund in StoreKit 2?
- Check the transaction's revocationDate. It is nil for a valid purchase and holds a date once the App Store refunds the transaction, so a non-nil revocationDate is the signal that a purchase was refunded or revoked.
- What is the difference between revocationDate and revocationReason?
- revocationDate is when the App Store took the purchase back, and revocationReason is why. The reason is developerIssue when the customer cited a problem in your app and other for anything else.
- Does a refunded purchase still appear in currentEntitlements?
- No. Transaction.currentEntitlements excludes purchases the App Store has refunded or revoked, so a refunded product drops out of the customer's entitlements on its own, which makes it a safe gate for access.
- Will StoreKit tell my app about a refund that happened while the app was closed?
- Only if you listen from launch. Transaction.updates delivers those changes once at startup, so you must start a Task iterating it as your app launches or the refund is missed until something else reconciles it.
- Is client-side refund detection enough on its own?
- No. The device only learns about a refund while your app is running, so a customer who never reopens the app is invisible to it. App Store Server Notifications V2 REFUND is the signal that reaches you regardless.
- Does a revocationDate always mean the customer was refunded?
- No. revocationDate is also set when a customer loses a purchase through Family Sharing, so a non-nil date means they no longer have the purchase but not always that money was returned.
Sources and further reading
- Apple Developer: Transaction.revocationDate (StoreKit)
- Apple Developer: Transaction.RevocationReason (StoreKit)
- Apple Developer: Transaction.currentEntitlements (StoreKit)
- Apple Developer: Transaction.updates (StoreKit)
- Apple Developer: App Store Server Notifications V2 notificationType (REFUND, REFUND_REVERSED)
- Apple Developer Tech Talk: Support customers with StoreKit 2 and App Store Server API
RefundHalt
The refund autopilot for the App Store and Google Play
Keep reading
Every App Store and Google Play refund window is a countdown, and here's how many hours each one gives you
Every App Store and Google Play refund starts a clock, and most of them run without you. Apple's shortest refund window is 12 hours, Google's chargeback window is 24, and from August 3, 2026 a missed chargeback window is a bill, not just a lost sale. Here is every deadline that touches your account.
A voided purchase notification lets your Google Play server revoke access the moment a refund lands
Google Play can push your server a voided purchase notification the instant a purchase is refunded, charged back, or voided. It carries a purchaseToken, orderId, productType, and refundType, and it means one thing, revoke access. Here is how to read it and wire it up.