你的 App 可以直接彈出應用內退款申請表,客戶點擊提交後 Apple 會這樣處理
Apple 的應用內退款申請讓客戶無需離開你的 App 就能申請退款,表單由 Apple 建立並審核。本文講清 beginRefundRequest 回傳什麼、它會在你的伺服器上啟動哪些 CONSUMPTION_REQUEST 和 48 小時計時,以及這個按鈕是否值得上線。

重點摘要
- Apple 的 beginRefundRequest 是一個 StoreKit 2 方法,它會在你的 App 內彈出 Apple 自己的退款表單。客戶看到自己的購買詳情和一組原因代碼,選擇其中一個,申請便提交給 Apple。表單不由你建立,結果也不由你決定。
- 該呼叫回傳 success 或 userCancelled 狀態,或拋出 duplicateRequest 或 failed 錯誤。success 狀態只表示 App Store 收到了申請,並不代表已核准。切勿在 success 時在介面上顯示退款已確認。
- 客戶提交後,Apple 最多需要 48 小時來核准或拒絕。對於消耗型購買項目,它會先向你的伺服器發送一個 CONSUMPTION_REQUEST,如果客戶已同意,你有慣常的 12 小時用使用資料來回應。
- 結果會以 App Store Server Notification 的形式送達你的伺服器,也就是你早已接收的那條通知流。核准對應 REFUND 通知,拒絕對應 REFUND_DECLINED。應用內申請進入這條通知流的方式,與客戶在 Apple 的 reportaproblem 頁面發起的退款完全一致。
- 該按鈕從 iOS 15 和 iPadOS 15、Mac Catalyst 15 以及 visionOS 1 起可用,因此任何面向這些版本的 App 今天就能彈出它。
- 從財務角度看,一筆你能申辯的退款,勝過一筆你無法申辯的拒付。把客戶留在 Apple 的流程裡會觸發一個你能回應的 CONSUMPTION_REQUEST,而不是一筆最終生效、還要收取手續費的銀行拒付。
- Apple 的擺放建議是從帳戶設定或說明選單裡呼叫它,而非放在購買頁面,這樣不滿的客戶能找到它,又不會向其他所有人宣傳退款。
Apple 允許客戶在完全不離開你的 App 的情況下申請退款。一個 StoreKit 呼叫 beginRefundRequest 就會在你的介面內彈出 Apple 自己的退款表單,客戶選好一個原因,申請便交給 Apple 審核。表單不由你建立,錢款你碰不到,結果也不由你決定。你得到的,是把退款入口放在心懷不滿的客戶所在之處的能力,而不是把他們流失給銀行。這就是應用內退款申請,在你決定是否上線這個按鈕之前,值得先弄清它是什麼。
下面這部分才關係到你的收入。這個按鈕本身不會退任何錢。它開啟一次申請,Apple 最多花 48 小時來核准或拒絕,而對於消耗型購買,它會先向你的伺服器發出一個 CONSUMPTION_REQUEST。所以這張表單不是白送。它是一個漏斗,把客戶導入你早已能施加影響的那套退款審核,還能在爭議變成你無法申辯的拒付之前,把它從信用卡網路裡拉回來。
應用內退款申請表單到底是什麼
beginRefundRequest 是一個 StoreKit 2 方法,它會在一個 window scene 中為某筆交易彈出退款申請表單。簽章很簡短:func beginRefundRequest(in scene: UIWindowScene) async throws -> Transaction.RefundRequestStatus。當你呼叫它時,系統會顯示一張表單,列出客戶的購買詳情和一組供其選擇的原因代碼。這套介面由 Apple 建立和掌控。你只提供 scene 和交易,別無其他。
Apple 對擺放位置的建議很明確。從帳戶設定或說明選單裡呼叫這個函式,這樣想退款的客戶就能在他們尋求支援的地方找到它。它隨 iOS 15 和 iPadOS 15、Mac Catalyst 15 以及 visionOS 1 一同發布,因此任何面向這些版本的 App 今天就能彈出它。
開啟表單的兩種方式
有兩個入口。你可以對一筆你已持有的具體交易呼叫 beginRefundRequest(in:),這會把表單限定在那一筆購買上。當你希望客戶退掉某個產品的購買時,也可以按產品識別碼開啟表單。無論哪種方式,表單、原因列表和裁決都歸 Apple 所有。你的職責止於彈出它並讀取結果。
呼叫回傳什麼,以及哪裡可能出錯
這個方法是 async throws,因此它要麼回傳一個狀態,要麼拋出一個錯誤。兩者都是很短的清單,也都值得處理,好讓表單關閉後你的介面能說出真實的情況。
| 結果 | 類型 | 含義 |
|---|---|---|
| success | RefundRequestStatus | App Store 已收到退款申請。它已提交,尚未核准 |
| userCancelled | RefundRequestStatus | 客戶未提交便關閉了表單。什麼都沒發送 |
| duplicateRequest | RefundRequestError | App Store 已存在這筆購買的退款申請 |
| failed | RefundRequestError | 提交本身失敗了。讓客戶再試一次 |
客戶點擊提交後,你的伺服器上會發生什麼
表單關閉是流程的開始,而非結束。Apple 會審核申請,最多花 48 小時核准或拒絕。對於消耗型應用內購買,在裁決之前,App Store 會向你的伺服器發送一個 CONSUMPTION_REQUEST 通知,索要使用資料。如果客戶已同意分享該資料,你透過 Send Consumption Information 端點回應。如果他們未同意,Apple 自己的指示是根本不要回應這條通知。
一旦 Apple 作出裁決,結果便以 App Store Server Notification 的形式落到你的伺服器上。這就是你早已接收的那條通知流,應用內申請進入它的方式,與客戶在 Apple 的 reportaproblem 頁面發起的退款完全一致。申請雖在你的 App 內發起,處理方式卻毫無不同。
| 階段 | 觸發什麼 | 你的動作 | 計時 |
|---|---|---|---|
| 客戶提交表單 | beginRefundRequest 回傳 success | 記錄下來,顯示待處理,而非已退款 | 即時 |
| 僅限消耗型,Apple 先詢問 | CONSUMPTION_REQUEST 通知 | 若客戶已同意則發送消耗資料,否則保持沉默 | 12 小時內回應 |
| Apple 核准 | REFUND 通知 | 撤銷該筆交易的權益 | 最多 48 小時裁決 |
| Apple 拒絕 | REFUND_DECLINED 通知 | 保留這筆銷售,什麼都不改 | 最多 48 小時裁決 |

這個按鈕讓你付出什麼,又能替你省下什麼
一筆你能申辯的退款,勝過一筆你無法申辯的拒付
在你的 App 內找不到退款入口的客戶不會就此放棄。他們會去找銀行。信用卡拒付在銀行處是最終生效的,它要收取一筆爭議手續費,還會把裁決權從你和 Apple 雙方手中都拿走。應用內退款申請把同一位客戶留在 Apple 的系統裡,在那裡,一筆消耗型購買會觸發一個你能回應的 CONSUMPTION_REQUEST,以及一個你能施加影響的裁決。用一筆可申辯的 Apple 審核換掉一筆不可申辯的拒付,就是這個按鈕全部的財務理由。
你正在降低退款的門檻
誠實的另一面是,一個顯眼的、一鍵可達的退款入口,會比一封埋在深處的支援郵件帶來更多退款申請。其中有些申請原本根本不會發生。這是實實在在的代價,也正因如此,Apple 才告訴你把入口放在帳戶設定或說明選單裡,而不是購買頁面上。你想讓已經不滿的客戶找到它,而不是讓僅僅好奇的客戶找到它。
從頭到尾一直在燒錢的成本,是繼續為一個已退款的帳戶提供服務
無論裁決走向如何,交付這一側的計價表都會一直轉,直到你根據結果採取行動。一個已退款的權益每多存活一小時,你就得繼續為它背後的真實成本買單:算力、模型 API 呼叫、儲存,以及任何與該客戶用量掛鉤的創作者或合作方分潤。應用內申請改變不了這一點。收到 REFUND 通知時立即撤銷才能改變。這個按鈕有多便宜,取決於你如何處理它最終產生的那條通知。
你該不該上線應用內退款申請
把它放在支援所在之處,而非銷售所在之處
遵循 Apple 的擺放建議。帳戶設定和說明選單才是合適的歸處。付費牆旁邊的一個退款連結會訓練人們期待拿回錢,還會招來那種你本無需提供的好奇型退款。
在信任它之前,先在沙盒裡測試完整流程
你可以在沙盒和 Xcode 的 StoreKit 測試中模擬整條路徑,把一個申請從待處理推向已核准或已拒絕。核准會向你的伺服器投遞一個 REFUND 通知,拒絕則投遞 REFUND_DECLINED,因此你能在真實客戶點擊提交之前,先證明你的處理程式反應正確。
處理好每一種結果,絕不誇大其詞
在 success 時顯示待處理,在 failed 時提供重試,在 userCancelled 時說明什麼都沒變,並把 duplicateRequest 當作一條平靜的提示,表示客戶先前的申請仍然有效。唯一會造成傷害的錯誤,是在你手裡只有一份已提交的申請時,卻告訴客戶退款已經辦妥。
RefundHalt 如何處理後續
應用內表單是 Apple 的。它之後的一切是你的,而那正是 RefundHalt 負責運行的部分。當客戶從你的 App 內提交退款時,RefundHalt 會為消耗型購買捕獲 CONSUMPTION_REQUEST,並在 12 小時視窗內用有助於 Apple 裁決的用量證據回應它。當 Apple 作出裁決,它會在 REFUND 時撤銷、在 REFUND_DECLINED 時原封不動地保留存取權,每一步都精確對應那筆交易。你既能提供更友善的應用內退款入口,又不必把審核、證據或撤銷留給手忙腳亂的人工處理。
常見問題解答
- beginRefundRequest 是做什麼的?
- 它會在你的 App 內為某筆具體交易彈出 Apple 的退款申請表單。客戶看到自己的購買詳情和一組原因代碼,選擇其中一個,申請便交給 Apple。該方法回傳 success 或 userCancelled 狀態,或拋出 duplicateRequest 或 failed。它本身不會退掉這筆購買,因為 Apple 會審核申請,最多花 48 小時裁決。
- 應用內退款申請會立即退錢嗎?
- 不會。success 結果表示 App Store 收到了申請,而非已核准。Apple 最多花 48 小時核准或拒絕,對於消耗型購買,它會先透過 CONSUMPTION_REQUEST 通知向你的伺服器索要使用資料。在 success 時給客戶顯示一個待處理狀態,絕不要顯示退款已確認。
- 哪個 iOS 版本支援應用內退款申請?
- iOS 15 和 iPadOS 15、Mac Catalyst 15 以及 visionOS 1。StoreKit 2 方法 beginRefundRequest(in:) 從這些版本起可用,因此任何面向 iOS 15 或更高版本的 App 都能從 App 內彈出 Apple 的退款表單。
- 我該把應用內退款按鈕放在哪裡?
- Apple 的建議是從帳戶設定或說明選單裡呼叫它,而非購買頁或付費牆頁面。這樣退款入口就放在了不滿客戶尋求支援的地方,又不會向那些本不打算退款的客戶宣傳退款。
- 應用內退款是否優於客戶去聯繫銀行?
- 就你的收入而言,通常是的。銀行拒付是最終生效的,還要收取手續費,並把 Apple 和你都排除在裁決之外。應用內退款申請把客戶留在 Apple 的流程裡,在那裡,一筆消耗型購買會觸發一個你能回應的 CONSUMPTION_REQUEST,以及一個你能施加影響的審核。一筆可申辯的退款勝過一筆不可申辯的拒付。
資料來源與延伸閱讀
- Apple Developer: beginRefundRequest(in:)
- Apple Developer: Transaction.RefundRequestStatus
- Apple Developer: Transaction.RefundRequestError
- Apple Developer: App Store Server Notifications V2 notificationType
- Apple Developer: Send Consumption Information
- Apple Developer: Support customers and handle refunds (WWDC21)
RefundHalt
App Store 與 Google Play 的退款自動駕駛
繼續閱讀
過多的退款申請,代價可能遠超退款本身,因為兩大 App 商店都把它視為品質訊號
Apple 審查準則 5.6.4 把過多的退款申請列為 App 未達品質標準的跡象,Google 的開發人員發布協議也允許它依退款率下架新開發人員的 App。本文整理兩家商店的原文、下架的實際成本,以及如何避開風險。
在處理退款之前先驗證 App Store 伺服器通知,因為任何人都能向你的 webhook URL 發送請求
在檢查簽章之前,一則退款通知只不過是一個 HTTPS 請求。本文說明如何驗證 App Store 伺服器通知和 Google Play RTDN 訊息、各商店簽署的內容,以及偽造的退款或遭錯誤拒絕的退款會讓你付出什麼代價。