All articles
Deep dive8 min read

Revoke Access After a Refund: The Step Apple and Google Don't Take for You

Apple and Google can both send a refund out the door while the customer keeps their purchase. Here is exactly when access is removed automatically, when your server has to do it, and the one notification most integrations never handle.

A smartphone glowing on a desk at night, illustrating the moment access to a refunded purchase is not yet revoked

Key takeaways

  • On Google Play, refunding an order does not remove the user's access by itself. Access is removed only when "Remove entitlement" is checked in Play Console, or the API call passes revoke=true, which is not the default.
  • A refund issued without the revoke option never appears in the Voided Purchases API at all. A revocation job that reads only that feed will silently miss every one of those orders.
  • Apple can revoke a license for non-consumables and subscriptions automatically, but a consumable is already considered delivered the moment it is spent, so StoreKit has no mechanism to revoke it. Your server has to deduct the balance itself.
  • Apple can also undo its own refund. A REFUND_REVERSED notification means a previously granted refund was reversed, revocationDate goes back to null, and any access or balance you removed has to be restored.
  • Google's voidedReason field carries nine values, from remorse and accidental_purchase to fraud, friendly_fraud, and unacknowledged_purchase, so the same feed that tells you to revoke access also tells you which revocations are abuse and which are your own integration bug.
  • Every day a refunded purchase keeps its access is a day you are paying for the compute, storage, and API calls behind it on money you have already given back.

Approving a refund and removing access are two separate steps, and only one of them happens automatically. Developers who assume a processed refund equals a revoked purchase are wrong on both major stores, in different ways and for different reasons. Knowing exactly where the automation stops is what it takes to revoke access after a refund instead of discovering months later that it never happened.

Why a refund does not revoke access by default

Apple and Google both treat the money and the access as two different operations. The money side is store policy: once a refund is approved, it is final and the ledger updates. The access side is optional, or the developer's job, or both, depending on the platform and the purchase type. Neither store assumes you want access gone the instant the refund clears, because plenty of legitimate cases (a partial refund, a goodwill credit, a support gesture) are not supposed to take the product away at all.

Google Play: the checkbox nobody notices

In Play Console, refunding an order is a two-part form: pick the refund amount, then decide separately whether to check "Remove entitlement." Leaving it unchecked is the default path most support agents take, because most refund requests are goodwill gestures where the customer keeps what they bought. The problem is that nothing in the refund confirmation screen tells you this is also how a customer keeps a subscription or in-app item after a refund you meant to be final.

The API path has the same trap

The same choice exists in code. The orders.refund endpoint takes an optional revoke query parameter; when it is true, access to the item is terminated immediately, and for a recurring subscription every future payment is cancelled with it. Omit the parameter and it defaults to false: the refund posts, the purchase record updates, and the user keeps using the product exactly as before.

ActionAccess removed?Appears in Voided Purchases API?
Play Console refund, "Remove entitlement" uncheckedNoNo
Play Console refund, "Remove entitlement" checkedYes, immediatelyYes
orders.refund API, revoke parameter omitted (defaults false)NoNo
orders.refund API, revoke=trueYes, immediatelyYes

That last row is the one that catches integrations. The Voided Purchases API exists to let you build a revocation pipeline without polling Play Console by hand, but it only lists orders that were actually voided with revocation. A refund processed without the checkbox, or without revoke=true, is invisible to that feed by design, not by bug.

A server rack with a single red status light among rows of green, representing a silent revocation decision

Reading the voidedReason field When a purchase does show up as voided, the voidedReason field tells you why, on a nine-value scale: other, remorse, not_received, defective, accidental_purchase, fraud, friendly_fraud, chargeback, and unacknowledged_purchase. That is not just bookkeeping. Remorse and accidental_purchase are ordinary refunds. Fraud, friendly_fraud, and chargeback are the ones worth routing to a fraud review queue instead of a plain revoke-and-forget path, because the same account is statistically more likely to repeat.

One operational detail matters if you build against this feed directly: purchases.voidedpurchases.list is capped at 6,000 queries a day and 30 queries in any 30-second window. A design that polls in a tight loop hits that ceiling fast; the intended pattern is a scheduled pull with pagination tokens, not continuous polling.

Apple: the App Store can revoke a license, but not a consumable

Apple's App Store Server Notifications V2 send a REFUND notification the moment a refund clears, for consumables, non-consumables, auto-renewable subscriptions, and non-renewing subscriptions alike. What happens after that notification depends entirely on the purchase type, because StoreKit's own revocation mechanism only covers two of the four.

Why consumables are the exception

A non-consumable or a subscription has a standing entitlement that Apple's systems can flip off. A consumable does not: once it is used, it is treated as delivered, and there is nothing left in StoreKit to revoke. If a customer bought 500 coins, spent them, and got a refund, the coins are already gone from Apple's point of view. The only ledger that still has them is yours. The REFUND notification is your one signal to find the matching original transaction ID and deduct that balance from your own records; nothing on Apple's side will do it for you.

NotificationWhat it meansWhat your server must do
REFUNDApple refunded a consumable, non-consumable, auto-renewable, or non-renewing purchaseRevoke non-consumable and subscription access immediately; deduct the consumable balance yourself
REFUND_DECLINEDApple declined a customer's refund requestNothing. Access was never at risk
REFUND_REVERSEDApple reversed a refund it had already grantedRestore whatever access or balance the REFUND notification caused you to remove
CONSUMPTION_REQUESTApple wants your evidence before it decides a refundRespond within 12 hours; this fires before a REFUND, not after one

The notification almost nobody handles

REFUND_REVERSED is the one most integrations skip entirely, because it describes Apple undoing its own decision, a case few developers design for on the first pass. When it arrives, revocationDate on the transaction goes back to null: as far as Apple is concerned, the purchase is valid again. If your server deleted an entitlement, cancelled a subscription record, or zeroed out a consumable balance when the original REFUND came in, REFUND_REVERSED is the signal to put all of it back.

How to close the gap, step by step

  • On Google Play, default every refund path to revoke=true, whether it goes through Play Console or the API, unless a specific support case calls for the customer keeping access.
  • Poll purchases.voidedpurchases.list on a schedule with pagination tokens, not a tight loop, and stay under 6,000 requests a day and 30 per 30 seconds.
  • Route voidedReason values of fraud, friendly_fraud, and chargeback to a review queue instead of an automatic revoke-and-close.
  • On Apple, subscribe to App Store Server Notifications V2 and revoke non-consumable and subscription access the moment a REFUND notification names them.
  • Store the original transaction ID against every consumable purchase so a REFUND notification can find the exact balance to deduct without a manual lookup.
  • Handle REFUND_REVERSED explicitly and log every revocation against its transaction or order ID, so a reversal can restore precisely what was removed.

What the gap actually costs

None of this is about fairness to the customer; it is about what keeps running after the money is gone. A refunded subscription that keeps calling your API, storing data, and consuming compute is not a moral problem, it is a line item: you paid the infrastructure bill for a purchase you already gave the revenue back on. The stores do not track that cost for you, because from their side the refund is complete the moment the money moves. The access side is entirely your bill to control.

This is the same reason RefundHalt treats revocation as part of the refund pipeline, not an afterthought: a REFUND event and an access change are recorded as one action, and a REFUND_REVERSED event restores state automatically instead of waiting for someone to notice the customer is still locked out of something Apple gave back.

Frequently asked questions

Does refunding a purchase on Google Play automatically revoke access?
No. Access is removed only if "Remove entitlement" is checked in Play Console or the orders.refund API call includes revoke=true. Neither is the default, so a refund can post while the customer keeps using the product.
What happens when a consumable in-app purchase is refunded on the App Store?
Apple sends a REFUND notification, but StoreKit has no way to revoke a consumable because it is considered delivered once spent. The developer's server must find the original transaction and deduct the balance manually.
What is the REFUND_REVERSED notification and does it need special handling?
REFUND_REVERSED means Apple reversed a refund it had already granted; revocationDate returns to null and the purchase is valid again. Any access or balance the original REFUND removed has to be restored, and most integrations never listen for this notification at all.
Why would a refunded Google Play order not appear in the Voided Purchases API?
The API only lists orders that were voided with revocation. If a refund was processed without the revoke option, the order is not returned by that endpoint at all, so a pipeline that relies solely on it will miss those refunds completely.
How often can I poll Google's Voided Purchases API?
The endpoint is rate-limited to 6,000 queries a day and 30 queries in any 30-second window, so it is meant for a scheduled pull with pagination tokens rather than continuous polling.

Sources and further reading

RefundHalt

The refund autopilot for the App Store and Google Play

Keep reading

The next refund request is already on its way.

Set up RefundHalt in the time it takes to read another support email about a refund you didn't get to contest.