Skip to content
Entitlements

Taking access back after a refund is work neither Apple nor Google does for you

Money moving and access ending are separate events on both stores, and only one happens automatically. Google leaves entitlement removal behind an off by default option; Apple can't touch a consumable already spent. Whatever your server misses keeps running on revenue you no longer have.

5 min read
Taking access back after a refund is work neither Apple nor Google does for you

Money moving and access ending are separate events on both platforms, and only one of them happens automatically. Google leaves entitlement removal behind an option that defaults to off. Apple handles licences but cannot touch a consumable somebody already spent. Whatever your server fails to clean up keeps running on revenue you no longer have.

Key takeaways

  • Refunding a Play order leaves the customer's access untouched unless you say otherwise. Either "Remove entitlement" gets ticked inside Play Console, or your API call carries revoke=true. Neither happens on its own.

  • Refunds issued without that revoke option are absent from the Voided Purchases API entirely. Any revocation job built solely on that feed will quietly overlook every single one.

  • Licences for non-consumables and subscriptions are something Apple can withdraw for you. Consumables are counted as delivered the moment they are spent, leaving StoreKit nothing to withdraw, so deducting the balance falls to your own server.

  • Apple reverses its own refunds sometimes. A REFUND_REVERSED notification says a granted refund has been undone, revocationDate returns to null, and everything you stripped away needs putting back.

  • Nine distinct values populate Google's voidedReason, spanning remorse and accidental_purchase through to fraud, friendly_fraud, and unacknowledged_purchase, so one feed both instructs you to cut access and flags which cases smell like abuse.

  • For as long as a refunded purchase retains access, you are funding its compute, its storage, and its API calls out of revenue that has already gone back to the buyer.

Why access survives a refund by default

Both companies treat the payment and the entitlement as separate operations. Payment follows store policy, and once approved a refund is settled and the ledger reflects it. Entitlement is a different matter, sometimes optional, sometimes yours to handle, frequently both depending on platform and product type. Neither store presumes that returning money means terminating access, because plenty of legitimate situations, a partial refund, a goodwill credit, a gesture from support, are meant to leave the customer holding what they bought.

Google Play: the tick box that goes unnoticed

Play Console splits refunding into two decisions. First the amount, then, quite separately, whether "Remove entitlement" gets ticked. Most support agents leave it alone, which is reasonable given that most refund requests genuinely are goodwill and the customer is meant to keep the goods. What the confirmation screen never mentions is that this same untouched box is how someone walks away still holding a subscription or an in-app item after a refund you intended to be conclusive.

The same trap exists in the API

Code presents the identical fork. An optional revoke query parameter belongs to the orders.refund endpoint, and setting it true ends access to the item at once, cancelling every future payment along with it where a recurring subscription is involved. Leave the parameter out and false is what you get: the refund posts, the purchase record changes, and the customer carries on exactly as before.

  • Refund in Play Console with "Remove entitlement" left unticked, and access carries on. The order never reaches the Voided Purchases API either.

  • Refund in Play Console with "Remove entitlement" ticked, and access ends straight away. The order does show up in the Voided Purchases API.

  • Call orders.refund and leave the revoke parameter off, so it defaults to false, and access carries on. Again, nothing reaches the Voided Purchases API.

  • Call orders.refund with revoke=true, and access ends straight away, with the order appearing in the Voided Purchases API.

A revocation job reading nothing but the Voided Purchases API will never encounter a refund issued without the revoke flag. The record is not late. It is simply never coming, and the order sits outside that feed as though the refund had not occurred.

That final row is what breaks integrations. Building a revocation pipeline without manually watching Play Console is precisely what the Voided Purchases API is for, but its scope is orders genuinely voided with revocation. Refunds processed without the tick box, or without revoke=true, are invisible there by design rather than through any defect.

Making sense of voidedReason

Voided purchases arrive with a voidedReason explaining the cause across nine possible values: other, remorse, not_received, defective, accidental_purchase, fraud, friendly_fraud, chargeback, and unacknowledged_purchase. Treating that as mere record-keeping wastes it. Remorse and accidental_purchase describe unremarkable refunds. Fraud, friendly_fraud, and chargeback deserve a fraud review queue rather than an automatic revoke-and-close, because accounts producing those values repeat at a statistically higher rate.

Anyone building directly against this feed needs one operational number: purchases.voidedpurchases.list permits 6,000 queries daily and 30 within any 30-second span. Tight polling loops exhaust that allowance quickly. Scheduled pulls using pagination tokens are the intended design.

Apple: licences can be withdrawn, consumables cannot

App Store Server Notifications V2 deliver a REFUND notification the instant a refund settles, covering consumables, non-consumables, auto-renewable subscriptions, and non-renewing subscriptions without distinction. What ought to happen next diverges completely by product type, because only two of those four fall within StoreKit's own revocation machinery.

The reason consumables sit outside it

Standing entitlements belong to non-consumables and subscriptions, and Apple's systems can switch those off. Consumables carry no such thing. Spending one marks it delivered, which leaves StoreKit with nothing left to reclaim. A customer who bought 500 coins, spent them, then obtained a refund has, from Apple's perspective, already received everything. Yours is the only ledger where those coins still exist. That REFUND notification is your single prompt to locate the matching original transaction ID and subtract the balance yourself, because nothing on Apple's side is going to.

  • REFUND means a consumable, non-consumable, auto-renewable, or non-renewing purchase has been refunded. Withdraw non-consumable and subscription access at once, and subtract any consumable balance yourself.

  • REFUND_DECLINED means Apple turned the customer's refund request down. There is nothing to do, since access was never in jeopardy.

  • REFUND_REVERSED means a refund Apple had already granted has been undone. Reinstate whatever the original REFUND notification led you to remove.

  • CONSUMPTION_REQUEST means Apple wants your evidence before it decides. Reply inside 12 hours, and note that this one arrives before a REFUND rather than after it.

The notification almost everyone ignores

REFUND_REVERSED is the one integrations routinely omit, since it represents Apple undoing a decision of its own, which is rarely something anyone designs for initially. Its arrival returns revocationDate to null, meaning Apple now considers the purchase perfectly valid again. Anything your server did on the original REFUND, deleting an entitlement, cancelling a subscription record, zeroing a consumable balance, needs reversing.

Do not expect a reversed refund to resemble a fresh purchase, because it will not. Bans applied, access withdrawn, balances marked spent: REFUND_REVERSED is the instruction to undo precisely those things, and no follow-up notification will arrive later to remind you.

Closing the gap, step by step

  • Make revoke=true the default across every Play refund route, Console and API alike, reserving the alternative for support cases that specifically warrant the customer keeping access.

  • Pull purchases.voidedpurchases.list on a schedule using pagination tokens rather than looping tightly, keeping beneath 6,000 requests daily and 30 per 30 seconds.

  • Send voidedReason values of fraud, friendly_fraud, and chargeback into a review queue instead of automatic revoke-and-close handling.

  • Subscribe to App Store Server Notifications V2 on Apple, and withdraw non-consumable and subscription access as soon as a REFUND notification identifies them.

  • Keep the original transaction ID stored against every consumable purchase, so a REFUND notification can locate the exact balance to subtract without anybody looking it up manually.

  • Implement REFUND_REVERSED deliberately, and record every revocation against its transaction or order ID, so a reversal restores exactly what was taken.

What the gap actually costs

Customer fairness is not the issue here. The issue is what carries on running once the revenue has gone. A refunded subscription still hitting your API, still holding data, still burning compute presents no moral difficulty whatsoever; it presents an invoice. Infrastructure was paid for on a purchase whose revenue you have already returned. Neither store tracks that expense on your behalf, because from where they sit the refund concluded the instant the money moved. Controlling the access side is entirely your expense to manage.

Refund Sensor therefore treats revocation as part of the refund pipeline rather than something bolted on afterwards. A REFUND event and its corresponding access change are written as a single action, and a REFUND_REVERSED event restores the prior state on its own, instead of waiting for somebody to notice a customer locked out of something Apple handed back days ago.

Where these rules are documented

Frequently asked questions

It does not. Access ends only where "Remove entitlement" was ticked in Play Console, or the orders.refund call carried revoke=true. Since neither is the default, refunds regularly post while customers continue using the product.

A REFUND notification arrives, but StoreKit cannot revoke a consumable because spending it counts as delivery. Locating the original transaction and subtracting the balance is work your own server has to perform.

It means a refund Apple already granted has been undone. revocationDate goes back to null and the purchase counts as valid once more. Whatever access or balance the original REFUND caused you to remove must be reinstated, and a great many integrations never subscribe to this notification at all.

Because that endpoint returns only orders voided with revocation. Refunds processed without the revoke option are not included, so any pipeline depending on that feed alone will overlook them entirely.

Six thousand queries per day, and no more than 30 inside any 30-second window. The endpoint is designed for scheduled pulls with pagination tokens rather than continuous polling.

#refunds#entitlements#voided purchases#refund_reversed#revoke access#play billing
Refund SensorRefund Sensor TeamRefund defense for App Store and Google Play developers