你的 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 的退款自動駕駛
繼續閱讀
當 Google Play 的購買被退款或被 chargeback 時,Voided Purchases API 就是你得知此事的方式
當購買被退款或被 chargeback 時,Google Play 會悄悄地作廢它。Voided Purchases API 就是這些訂單的清單,讓你可以撤銷存取權。這裡有每一個欄位、30 天的時間窗、會隱藏訂單的 revoke 選項,以及它的成本。
蘋果做出裁定後會發出三種 App Store 退款通知,而 REFUND_REVERSED 會把這筆銷售還給你
蘋果透過 App Store Server Notifications V2 發送四種退款訊息,而大多數應用程式只處理其中兩種。REFUND 告訴你要撤銷權益,REFUND_DECLINED 意味著保留這筆銷售,REFUND_REVERSED 則把銷售還給你,並要求你恢復先前撤銷的內容。以下說明每一種各需要什麼處理。