退款後撤銷存取權限:Apple 和 Google 不會替你完成的那一步
Apple 和 Google 都可能核准退款,但顧客手上的購買項目卻原封不動。這篇文章會精確說明什麼情況下系統會自動撤銷存取權限,什麼情況下要靠你的伺服器自己處理,以及那個幾乎所有整合都會漏接的通知。

重點摘要
- 在 Google Play 上,退款一筆訂單本身並不會撤銷使用者的存取權限。只有在 Play Console 中勾選「Remove entitlement」,或是 API 呼叫帶入 revoke=true 時,存取權限才會被撤銷,而這並不是預設值。
- 沒有勾選 revoke 選項的退款,完全不會出現在 Voided Purchases API 裡。只讀取這個資料來源的撤銷作業,會無聲無息地漏掉每一筆這樣的訂單。
- Apple 可以自動撤銷 non-consumable 項目和訂閱的授權,但 consumable 一旦被使用,就會被視為已經交付完成,因此 StoreKit 沒有機制可以 revoke 它。你的伺服器必須自行扣除餘額。
- Apple 也可能撤回自己核准的退款。REFUND_REVERSED 通知代表先前核准的退款被撤回了,revocationDate 會回到 null,而你先前移除的任何存取權限或餘額都必須恢復。
- Google 的 voidedReason 欄位共有九種數值,從 remorse 和 accidental_purchase,到 fraud、friendly_fraud 和 unacknowledged_purchase 都有。同一份資料來源除了告訴你該 revoke 存取權限之外,也告訴你哪些撤銷是濫用行為,哪些只是你自己整合上的錯誤。
- 退款項目每多保留存取權限一天,你就多為背後的運算、儲存和 API 呼叫多付一天的錢,而這筆錢的收入你早就已經退還給顧客了。
核准退款和移除存取權限是兩個獨立的步驟,而且只有其中一個是自動發生的。認為退款一旦處理完成就等於購買項目已被撤銷的開發者,在兩大應用商店上都想錯了,而且錯的方式和原因各不相同。要在退款後 revoke 存取權限,而不是幾個月後才發現這件事根本沒發生,關鍵就在於清楚知道自動化到底在哪裡停下。
為什麼退款預設不會 revoke 存取權限
Apple 和 Google 都把金流和存取權限當成兩件不同的事來處理。金錢的部分是商店政策,退款一旦核准,就是最終結果,帳目會立刻更新。存取權限的部分則是選擇性的,或是開發者自己的責任,或兩者皆是,視平台和購買類型而定。兩家商店都不會假設你希望退款一結清,存取權限就馬上消失,因為有很多正當的情況,例如部分退款、善意抵免、客服的體貼作法,本來就不該把產品收回。
Google Play:沒人注意到的核取方塊
在 Play Console 中,退款一筆訂單是一份分兩部分的表單,先選擇退款金額,再另外決定是否勾選「Remove entitlement」。大多數客服人員預設不勾選,因為大部分退款請求都是善意的處理方式,讓顧客保留他們買的東西。問題是退款確認畫面上完全沒有任何提示告訴你,這也代表著顧客在一筆你原本以為已經結案的退款之後,仍然能保留訂閱或應用程式內項目。
透過 API 的路徑也有同樣的陷阱
程式碼裡也存在同樣的選擇。orders.refund 端點接受一個可選的 revoke 查詢參數,當它是 true 時,該項目的存取權限會立即終止,對於定期訂閱來說,未來每一筆付款也會一併取消。如果省略這個參數,預設值就是 false,退款會照常送出,購買紀錄會更新,而使用者則完全照舊繼續使用該產品。
| 動作 | 存取權限是否被移除? | 是否出現在 Voided Purchases API 中? |
|---|---|---|
| 透過 Play Console 退款,未勾選「Remove entitlement」 | 否 | 否 |
| 透過 Play Console 退款,已勾選「Remove entitlement」 | 是,立即生效 | 是 |
| orders.refund API,省略 revoke 參數(預設為 false) | 否 | 否 |
| orders.refund API,revoke=true | 是,立即生效 | 是 |
問題就出在最後那一列,這正是最常讓整合出包的地方。Voided Purchases API 的用意是讓你不必手動輪詢 Play Console,就能建立撤銷作業的流程,但它只列出真正以撤銷方式作廢的訂單。沒有勾選核取方塊,或沒有設定 revoke=true 的退款,在這個資料來源裡是刻意看不到的,不是系統的錯誤。

解讀 voidedReason 欄位 當一筆購買確實顯示為 voided 時,voidedReason 欄位會告訴你原因,總共有九種數值:other、remorse、not_received、defective、accidental_purchase、fraud、friendly_fraud、chargeback,以及 unacknowledged_purchase。這不只是記帳用的資訊。remorse 和 accidental_purchase 屬於一般退款。fraud、friendly_fraud 和 chargeback 則值得被導向詐欺審查佇列,而不是單純 revoke 之後就不管了,因為同一個帳號在統計上更有可能再犯。
如果你要直接串接這個資料來源,有一個維運細節很重要:purchases.voidedpurchases.list 的上限是每天 6,000 次查詢,以及任何 30 秒的區間內 30 次查詢。如果設計成緊密迴圈式的輪詢,很快就會撞到這個上限;正確的做法是搭配分頁權杖排程拉取資料,而不是持續不斷地輪詢。
Apple:App Store 可以 revoke 授權,但無法對 consumable 這麼做
Apple 的 App Store Server Notifications V2 會在退款完成的那一刻發出 REFUND 通知,無論是 consumable、non-consumable、自動續訂訂閱,還是非自動續訂訂閱都一樣。這則通知之後會發生什麼事,完全取決於購買類型,因為 StoreKit 本身的撤銷機制只涵蓋四種之中的兩種。
為什麼 consumable 是例外
non-consumable 或訂閱擁有一個持續存在的權利,Apple 的系統可以把它關掉。consumable 則不然,一旦被使用,就會被視為已經交付完成,StoreKit 裡再也沒有什麼可以拿來 revoke。如果顧客買了 500 枚代幣,已經用掉了,然後拿到退款,從 Apple 的角度來看,這些代幣早就沒了。唯一還留著這筆帳的,只有你自己的帳本。REFUND 通知是你唯一的訊號,讓你去找出對應的 original transaction ID,然後從自己的紀錄裡扣掉那筆餘額,Apple 那邊不會替你處理任何事。
| 通知 | 代表的意思 | 你的伺服器必須做的事 |
|---|---|---|
| REFUND | Apple 已對 consumable、non-consumable、自動續訂或非自動續訂的購買項目退款 | 立即 revoke non-consumable 和訂閱的存取權限;consumable 的餘額則自行扣除 |
| REFUND_DECLINED | Apple 拒絕了顧客的退款請求 | 不需要做任何事。存取權限從未受到影響 |
| REFUND_REVERSED | Apple 撤回了先前已核准的退款 | 恢復因 REFUND 通知而被你移除的任何存取權限或餘額 |
| CONSUMPTION_REQUEST | Apple 想在決定是否退款前取得你提供的證據 | 在 12 小時內回覆;這個通知會在 REFUND 之前發出,而不是之後 |
幾乎沒人處理的那個通知
REFUND_REVERSED 是大多數整合會完全跳過的通知,因為它描述的是 Apple 撤回自己先前的決定,而這是很少開發者在第一輪設計時就會考慮到的情況。當它送達時,交易上的 revocationDate 會回到 null,就 Apple 而言,這筆購買重新生效了。如果你的伺服器在原本的 REFUND 送達時刪除了某項權利、取消了訂閱紀錄,或把 consumable 的餘額歸零,那麼 REFUND_REVERSED 就是要你把這一切全部復原的訊號。
如何一步步補上這個缺口
- 在 Google Play 上,無論是透過 Play Console 還是 API,都應該讓每一條退款路徑預設使用 revoke=true,除非有特定的客服情境需要讓顧客保留存取權限。
- 以排程搭配分頁權杖的方式輪詢 purchases.voidedpurchases.list,而不是用緊密迴圈,並將次數控制在每天 6,000 次以下、每 30 秒 30 次以下。
- 把 voidedReason 為 fraud、friendly_fraud 和 chargeback 的情況導向審查佇列,而不是自動 revoke 後直接結案。
- 在 Apple 這一側,訂閱 App Store Server Notifications V2,並在 REFUND 通知指名某個項目的當下,立刻 revoke 該 non-consumable 或訂閱的存取權限。
- 為每一筆 consumable 購買都保存好 original transaction ID,這樣 REFUND 通知一來,就能精準找到該扣除的餘額,不必手動查找。
- 明確處理 REFUND_REVERSED,並針對每一次撤銷都記錄對應的 transaction ID 或 order ID,這樣一旦被撤回,就能精準恢復當初被移除的內容。
這個缺口實際上要付出多少代價
這一切都與對顧客是否公平無關,而是關於錢已經退回去之後,還有什麼東西持續在運作。一筆已退款的訂閱,如果還在持續呼叫你的 API、儲存資料、消耗運算資源,這不是道德問題,而是一筆實實在在的支出項目,你正在為一筆早已把收入退還回去的購買,支付基礎設施的帳單。商店不會替你追蹤這筆成本,因為在他們看來,退款一旦金流動了就算完成了。存取權限這一側的帳,完全要靠你自己來控管。
這正是為什麼 RefundHalt 把撤銷存取權限當成退款流程的一部分,而不是事後才想到的補救措施,REFUND 事件和存取權限的變更會被記錄為同一個動作,而 REFUND_REVERSED 事件則會自動恢復狀態,不必等到有人發現顧客明明被 Apple 歸還了東西,卻依然被鎖在外面。
常見問題解答
- 在 Google Play 上退款一筆購買,會自動 revoke 存取權限嗎?
- 不會。只有在 Play Console 中勾選「Remove entitlement」,或是 orders.refund API 呼叫中帶入 revoke=true,存取權限才會被移除。這兩者都不是預設值,所以退款可以照常送出,而顧客仍然能繼續使用該產品。
- 在 App Store 上,consumable 應用程式內購買項目被退款時會發生什麼事?
- Apple 會發出 REFUND 通知,但 StoreKit 沒有辦法 revoke consumable,因為它一旦被使用就被視為已經交付完成。開發者的伺服器必須自行找出原始交易,並手動扣除餘額。
- REFUND_REVERSED 通知是什麼,需要特別處理嗎?
- REFUND_REVERSED 代表 Apple 撤回了先前已核准的退款,revocationDate 會回到 null,這筆購買重新生效。原本的 REFUND 所移除的任何存取權限或餘額都必須恢復,而大多數整合根本從來不監聽這個通知。
- 為什麼一筆已退款的 Google Play 訂單不會出現在 Voided Purchases API 裡?
- 這個 API 只會列出以撤銷方式作廢的訂單。如果退款是在沒有選擇 revoke 選項的情況下處理的,這個端點就完全不會回傳那筆訂單,因此只依賴這個 API 的流程,會完全漏掉那些退款。
- 我可以多頻繁地輪詢 Google 的 Voided Purchases API?
- 這個端點的速率限制是每天 6,000 次查詢,以及任何 30 秒區間內 30 次查詢,因此它的設計用途是搭配分頁權杖排程拉取資料,而不是持續輪詢。
資料來源與延伸閱讀
- Apple: Handling refund notifications
- Apple: notificationType (App Store Server Notifications)
- Apple: revocationReason (App Store Server Notifications)
- Google: Voided Purchases API overview
- Google: purchases.voidedpurchases REST resource
- Google: orders.refund API reference (revoke parameter)
- Google Play Console Help: manage your app's orders and issue refunds
RefundHalt
App Store 與 Google Play 的退款自動駕駛
繼續閱讀
The refunds you cannot contest: what Apple and Google decide without asking you
Only two refund flows ever ask for your evidence: Apple's consumption request and Google Play's chargeback review. Everything else, the 48-hour Play refund, support refunds, voided purchases, unacknowledged trials, is decided without you. Here is the full map, what you can prevent, and what you can only record.
Apple's CONSUMPTION_REQUEST, explained: the 12 hours that decide your refund
When a customer asks Apple for a refund, Apple asks you first. Here's exactly what the CONSUMPTION_REQUEST notification contains, what Apple expects back, how to answer one end to end, and why most developers silently forfeit.