蘋果做出裁定後會發出三種 App Store 退款通知,而 REFUND_REVERSED 會把這筆銷售還給你
蘋果透過 App Store Server Notifications V2 發送四種退款訊息,而大多數應用程式只處理其中兩種。REFUND 告訴你要撤銷權益,REFUND_DECLINED 意味著保留這筆銷售,REFUND_REVERSED 則把銷售還給你,並要求你恢復先前撤銷的內容。以下說明每一種各需要什麼處理。

重點摘要
- 蘋果透過 App Store Server Notifications V2 發送四種與退款相關的訊息。CONSUMPTION_REQUEST 索取你的證據,而 REFUND、REFUND_DECLINED 和 REFUND_REVERSED 會在蘋果已經做出裁定後回報結果。
- REFUND 通知意味著 App Store 已對該筆交易退款。它攜帶 revocationDate 和 revocationReason,是你撤銷與該單筆交易綁定權益的訊號,而不是撤銷該產品的每一次購買。
- revocationReason 有兩個取值。1 表示因你的產品存在問題而核准退款,0 表示因其他原因核准退款。取值為 1 的情況是值得記錄並追蹤趨勢的品質訊號。
- REFUND_DECLINED 表示蘋果駁回了顧客的退款。你保留這筆銷售,無需改動任何東西,但前提是你在裁定成為最終結果之前沒有撤銷過存取權限,這樣才安全。
- REFUND_REVERSED 表示蘋果撤銷了它先前已核准的退款,通常發生在顧客對該退款提出異議之後。交易上的撤銷欄位會消失,蘋果自身的指引是,如果你撤銷過內容,就需要恢復它。
- 對全部四種通知都要以 HTTP 200 回應。如果你的伺服器曾當機而漏掉了某一條,Get Refund History 端點可以讓你按交易 id 查詢已退款的交易並進行對帳。
- 針對過去某個訂閱週期的退款並不總是意味著應當結束存取。如果一個更新的付費週期仍然有效,對舊交易執行撤銷會切斷一位仍在付費的顧客的服務。
蘋果對你的退款做出裁定後,它並不會就此沉默。一旦結果確定,App Store 會向你的伺服器發送三種 App Store 退款通知之一,每一種都要求不同的動作。REFUND 表示錢已經退掉,你應當收回存取權限。REFUND_DECLINED 表示顧客的請求失敗,你保留這筆銷售。REFUND_REVERSED 表示蘋果撤銷了它先前已經核准的退款,於是這筆銷售重新歸你所有,你需要歸還先前收回的一切。大多數應用程式只接好了第一種,而悄悄忽略了另外兩種。一位付費顧客就是這樣被鎖在了他們已經付過錢的東西之外。
這三種與 CONSUMPTION_REQUEST 不同,後者是唯一一種要求你回覆的退款訊息。裁定後的通知不想要爭辯。它們要的是一個 HTTP 200 以及對顧客存取權限的正確改動。以下說明每一種的含義、承載事實的確切欄位,以及處理不當時錢會從哪裡漏掉。
四種退款通知,以及哪一種需要回覆
App Store Server Notifications V2 是一條單一的資訊流。你只把它指向一個 URL,蘋果就會把每一種通知類型都發到那裡,所以無論你是否處理,你其實都已經收到了全部四種退款訊息。其中有四種類型涉及退款,而只有一種是提問。
| 通知 | 蘋果在告訴你什麼 | 你的動作 | 是否需要回覆 |
|---|---|---|---|
| CONSUMPTION_REQUEST | 顧客申請了退款,蘋果需要你的資料 | 在 12 小時內 Send Consumption Information | 需要,真實資料 |
| REFUND | App Store 已對該筆交易退款 | 撤銷該筆交易的權益 | 不需要,HTTP 200 |
| REFUND_DECLINED | App Store 駁回了退款 | 保留存取,不改動任何東西 | 不需要,HTTP 200 |
| REFUND_REVERSED | 蘋果撤銷了它已核准的退款 | 恢復你先前撤銷的內容 | 不需要,HTTP 200 |
REFUND 通知究竟在告訴你什麼
當 App Store 成功地向顧客退回了一筆交易時,REFUND 就會觸發。它適用於每一種購買類型:消耗型項目、非消耗型項目、自動續訂訂閱以及非續訂訂閱。通知內部經過簽署的交易,現在攜帶了退款之前沒有的兩個欄位,而這兩個欄位就是全部故事。
revocationDate 和 revocationReason 承載事實
revocationDate 是 App Store 對該筆交易退款或撤銷它的 UNIX 時間,以毫秒為單位。revocationReason 告訴你退款的類別,它恰好有兩個取值。
| revocationReason | 蘋果的含義 | 你應從中讀出什麼 |
|---|---|---|
| 1 | 因產品存在問題而核准退款 | 一個品質或交付訊號。記錄它,追蹤趨勢,並在某個產品或某個組建版本中尋找規律 |
| 0 | 因其他原因核准退款 | 一次普通退款。撤銷權益,然後繼續 |
一筆交易上出現 revocationDate 本身就是標誌。如果你之後取回一筆交易,發現它帶有 revocationDate,那麼無論有沒有通知,這次購買都已被退款。請同時讀取它的原因,這樣某個版本上出現的一波取值為 1 的退款潮就不會作為雜訊從你眼前溜過。
按交易撤銷,而不是按產品撤銷
這裡的陷阱是撤銷得太多。一條 REFUND 只指名一筆交易。它並沒有讓你停用顧客對該產品 id 曾經進行過的每一次購買。蘋果自身的指引是,在你切斷任何東西之前,先檢查顧客仍然持有哪些存取權限,因為權益之間會重疊。經典的情形是訂閱:退款落在上個月的續訂上,而這個月的續訂正處於有效且已全額付款的狀態。若你按產品撤銷,你就為一筆針對已經結束的週期的退款,切斷了一位當前仍在付費的顧客。
REFUND_DECLINED 意味著你已經贏了,所以不要把它撤回
當 App Store 駁回了顧客的退款請求時,REFUND_DECLINED 就會到來。顧客提出了申請,蘋果說不行,你保留這筆銷售。表面上沒有什麼要做的,而這正是關鍵。這條通知暴露的錯誤是另一個:過早撤銷存取權限。
如果你的程式碼在蘋果做出裁定之前就對 CONSUMPTION_REQUEST 做出反應、收回了顧客的存取權限,那麼 REFUND_DECLINED 就是那個決定爆雷的時刻。蘋果留下了你的錢,而你卻把一位退款被駁回的顧客鎖在了門外。這位顧客現在為一個他無法使用的產品付費,提交一張支援工單,並把這件事記在心裡。修復辦法是一條規則,而不是一個功能:在 REFUND 時撤銷,絕不在請求時撤銷。REFUND_DECLINED 只不過是蘋果在確認,過早撤銷本來就是錯誤的做法。
REFUND_REVERSED 是那條把錢還給你的通知
REFUND_REVERSED 是幾乎沒人處理的那一條,也是把錢還給你的那一條。當蘋果撤銷它先前已核准的退款時,它就會發送這條通知,通常發生在顧客對該退款提出異議之後。REFUND 先前給交易添加的撤銷欄位又被移除了,於是該筆購買再次顯示為已付款。蘋果用一句話說明了開發者的職責:如果你的應用程式因相關退款而撤銷了內容或服務,就需要恢復它們。它適用於任何購買類型,從消耗型項目到自動續訂訂閱。
數週之後才到來的問題
開發者在蘋果自己的論壇上提出的真正問題是時機。一條 REFUND_REVERSED 可能在最初的 REFUND 之後數週才到來,那時距訂閱週期到期早已過去很久。那時你還要不要恢復存取?恢復該筆交易實際授予的東西,範圍限定在該筆交易所涵蓋的內容之內。對於消耗型或非消耗型項目,把解鎖重新打開。對於已經過去的訂閱週期,你並不是在發放新的時長,而是在更正記錄,讓顧客的歷史準確無誤,並讓任何仍然有效的權益重新生效。恢復那一筆具體的交易,然後由你的重疊邏輯來判定當前哪些是有效的。

把這件事做對的價值在哪裡
這些通知中的每一條都對應一個真實的數字,而處理不當的代價並不只是銷售價格本身。
REFUND:別再為一位已退款的顧客付出服務成本
REFUND 一到,銷售價格就已經沒了。你仍然能控制的,是繼續交付所帶來的成本。一筆已退款的權益每多存活一小時,你就在為顧客不再付費的東西繼續花錢:運算、模型 API 呼叫、儲存,以及與其使用相關的任何創作者或合作方分潤。在 REFUND 時及時撤銷可以讓這個計費表停下來。忽略這條通知就意味著你在為一個商店已經補償過的人資助產品。
REFUND_DECLINED:別把一場勝利變成一次善意退款
當你過早撤銷、而退款之後又被駁回時,你在帳面上保住了這筆銷售,卻在實際中失去了它。付了錢的顧客無法使用產品,於是你接手了一場支援對話,並且往往還要給一筆酌情退款來把事情擺平。這是在為一筆從未有過風險的銷售付兩次錢。正確處理 REFUND_DECLINED 不需要任何成本,這正是為什麼在 REFUND 之前保持存取權限不動,是你能採納的最省錢的規則。
REFUND_REVERSED:最糟的組合是錢和存取權限都沒了
忽略 REFUND_REVERSED,你就會走向全盤最糟的結局。你已經收了錢,而顧客卻一無所有。他們已經聯繫過一次銀行來撤銷退款,而一個被鎖在如今仍要為其付費的產品之外的人,很可能會第二次聯繫銀行。下一次爭議可能演變為信用卡拒付,那是由銀行終裁的,代價比這筆銷售本身高得多。REFUND_REVERSED 一到就恢復存取權限,是整個退款流程中最便宜的保險。
需要接好哪些環節
一旦模型對了,處理起來其實很小。把權益以交易 id 為鍵,讓每一條通知都指向唯一一筆購買。收到 CONSUMPTION_REQUEST 時,在 12 小時內發送你的資料。收到 REFUND 時,撤銷那筆交易。收到 REFUND_DECLINED 時,什麼都不做。收到 REFUND_REVERSED 時,恢復。對所有通知都迅速返回 HTTP 200,然後在你自己的時間裡做存取權限的改動。
對於通知留下的缺口,使用 Get Refund History 端點。如果你的伺服器在一次故障中當機、漏掉了一條 REFUND,就針對某個交易 id 呼叫 App Store Server API 的退款查詢,路徑為 /inApps/v2/refund/lookup/{transactionId},並讀回帶有 revocationDate 和 revocationReason 的已簽署交易。它一次對一筆交易進行對帳,並翻頁瀏覽顧客的已退款購買,這樣一個漏掉的 webhook 就不會變成一個被永久設錯的權益。
這正是 RefundHalt 為你運行的部分。它監聽全部四種類型,在 REFUND 時撤銷,在 REFUND_DECLINED 時保持存取不動,並在 REFUND_REVERSED 時自動恢復,每一步都以確切的交易為鍵。一筆被撤銷的退款不會在佇列裡乾等而讓付費顧客一直被鎖在外面,而一筆被駁回的退款也絕不會觸發一次你還得回退的撤銷。
常見問題解答
- REFUND 和 REFUND_REVERSED 有什麼區別?
- REFUND 表示 App Store 對一筆交易進行了退款,你應當撤銷該權益;而 REFUND_REVERSED 表示蘋果撤銷了它已核准的退款,你應當恢復先前撤銷的內容。兩者成對出現:一筆購買可以先走 REFUND,之後如果顧客的異議被推翻,再走 REFUND_REVERSED。把你的存取權限改動以交易 id 為鍵,這樣每條通知都作用於正確的那筆購買。
- 收到 REFUND 通知需要回傳什麼嗎?
- 不需要。你對 REFUND、REFUND_DECLINED 和 REFUND_REVERSED 都以一個不帶內文的 HTTP 200 回應。只有 CONSUMPTION_REQUEST 要求你發送資料,且需在 12 小時內透過 Send Consumption Information 端點發送。另外三種是蘋果在回報一個裁定,而不是在提問。
- 收到 REFUND_DECLINED 通知時我該做什麼?
- 什麼都不改,因為顧客的退款被駁回了,你保留這筆銷售。REFUND_DECLINED 唯一會給你帶來工作量的情形,是你在蘋果裁定之前就過早撤銷了存取權限。在 REFUND 時撤銷,而不是在 CONSUMPTION_REQUEST 時撤銷,那麼 REFUND_DECLINED 就成了一個確認,說明存取權限被正確地保持了原狀。
- 當 REFUND_REVERSED 在退款數週後才到來時,我該恢復存取權限嗎?
- 是的,恢復該筆具體交易所授予的權益。蘋果說明,如果你的應用程式因相關退款而撤銷了內容,就需要恢復它。對於消耗型或非消耗型項目,把解鎖重新打開。對於已經過期的訂閱週期,你是在更正記錄,而不是發放新的時長,所以由你的重疊邏輯來判定當前哪些是有效的。
- 我該如何補上伺服器漏掉的退款通知?
- 使用 App Store Server API 的 Get Refund History 端點,它在 /inApps/v2/refund/lookup/{transactionId} 按交易 id 查詢某位顧客的已退款交易。它返回帶有 revocationDate 和 revocationReason 的已簽署交易,這樣在一次故障之後,你無需等待一條已經觸發過的通知就能對存取權限進行對帳。它每次呼叫處理一個交易 id,並翻頁瀏覽該顧客的已退款購買。
資料來源與延伸閱讀
RefundHalt
App Store 與 Google Play 的退款自動駕駛
繼續閱讀
過多的退款申請,代價可能遠超退款本身,因為兩大 App 商店都把它視為品質訊號
Apple 審查準則 5.6.4 把過多的退款申請列為 App 未達品質標準的跡象,Google 的開發人員發布協議也允許它依退款率下架新開發人員的 App。本文整理兩家商店的原文、下架的實際成本,以及如何避開風險。
在處理退款之前先驗證 App Store 伺服器通知,因為任何人都能向你的 webhook URL 發送請求
在檢查簽章之前,一則退款通知只不過是一個 HTTPS 請求。本文說明如何驗證 App Store 伺服器通知和 Google Play RTDN 訊息、各商店簽署的內容,以及偽造的退款或遭錯誤拒絕的退款會讓你付出什麼代價。