有一個端點能回傳某位客戶在 App Store 的全部退款記錄,本文說清楚它究竟交還了什麼
Apple 的 Get Refund History 端點會以簽章交易的形式,回傳某位客戶在 App Store 的完整退款記錄。本文講清每一個欄位、revision 權杖如何翻頁、為何它是按客戶而非按 App 維度,以及你漏掉一筆退款要付出的代價。

重點摘要
- Get Refund History 是 App Store Server API 的一個端點,它會把某位客戶在你 App 內已退款的應用程式內購買,以簽章交易清單的形式回傳。如此一來,即便通知從未送達,你也能對帳退款並撤銷存取權限。
- 你用該客戶的任意一筆交易 id 呼叫 GET /inApps/v2/refund/lookup/{transactionId},Apple 會回傳該客戶在你 App 內所有購買類型的退款,而不僅是你查詢的那一筆。
- 回應包含三個欄位:signedTransactions,每頁最多 20 筆 JWS 交易,按退款時間由舊到新排序;外加一個 revision 權杖與一個用於翻頁的 hasMore 布林值。
- 保存最後一個 revision 權杖。下次把它傳回去,Apple 只回傳比該時間點更新的退款,這樣每次執行就把整段歷史拉取,變成一小份新增記錄。
- 每筆解碼後的交易都帶有 revocationDate 與 revocationReason。revocationReason 為 1 表示客戶因你 App 中一個實際或感知到的問題而退款,為 0 則表示其他原因,例如誤購。
- 該端點是按客戶維度,而非按 App 維度。沒有哪個呼叫能列出你整個 App 的所有退款,所以你要從一筆交易 id 開始按帳戶對帳,或讀取你的 REFUND 通知流以取得全 App 視圖。
- 接上它的理由是錢。一筆你從未捕捉到的退款會讓帳戶繼續存活,而你還在為一個 App Store 早已補償過的客戶,持續支付運算、模型 API 呼叫、儲存與分潤。
對於你的 App,Apple 會為每位客戶帳戶上已核准的每一筆退款保留一份可查詢的記錄,一次呼叫即可回傳。這個端點就是 Get Refund History,屬於 App Store Server API,它會以簽章交易清單的形式,交還該客戶在 App Store 的完整退款記錄。你傳入一筆交易 id,就能拿回 Apple 退了哪些款,再拿它與你仍在開啟的權限做對帳。
為什麼值得費這個勁。一筆你從未看見的退款,就是一筆你還在為之付費的退款。錢已經沒了,但帳戶還活著,只要它活著,你每小時都在繼續支出運算、模型 API 呼叫、儲存,以及任何與該客戶綁定的分潤。你的退款通知本該在退款發生的那一刻就捕捉到它。Get Refund History 則是通知失靈時的兜底,在一次當機之後、一次弄丟了 webhook 的部署之後,或者在一樁需要用一次呼叫看清全貌的客服工單裡。
App Store 退款記錄端點會回傳什麼
你對 App Store Server API 呼叫 GET /inApps/v2/refund/lookup/{transactionId},用你呼叫其他所有端點時相同的 JWT 簽章。路徑中的交易 id 可以是該客戶的任意一筆交易。Apple 把它當作身分識別,而不是過濾條件,會回傳該客戶在你整個 App 內已退款的購買:消耗型、非消耗型、自動續訂與非續訂訂閱一併在內。此端點更舊的 V1 版本在單次回應中最多回傳 50 筆退款,現已淘汰。目前版本會翻頁,因此面對歷史很長的客戶,你也無需處理一個龐大的負載。
回應就是三個欄位
| Field | 內容 |
|---|---|
| signedTransactions | 該客戶最多 20 筆已退款交易,每筆都是你需驗證並解碼的簽章 JWS。按 revocationDate 由最早退款起排序。空陣列表示該客戶在你 App 內沒有退款 |
| revision | 分頁權杖。傳回去以取得下一頁,並保留最後一個,以便下次只拉取新增退款 |
| hasMore | 當 Apple 持有的已退款交易多於本頁回傳的數量時為 True,此時你用 revision 再次呼叫 |
一筆已退款交易能告訴你什麼
signedTransactions 中的每一項都是一個 JWS。用 Apple 的憑證鏈驗證它、解碼它,你就得到一個一般的交易負載,其中填好了退款欄位。下面這些是這裡真正要緊的。
| Field | 內容 |
|---|---|
| transactionId | 已退款交易的 id,你回連到已記錄購買的關聯鍵 |
| originalTransactionId | 交易鏈中第一筆購買的 id,你把一個訂閱的多次續訂串起來的依據 |
| productId | 被退款的產品,讓你只撤銷正確的權益,別的一概不動 |
| revocationDate | Apple 退款該交易的 UNIX 時間,單位為毫秒 |
| revocationReason | Apple 退款的原因。1 表示你 App 中一個實際或感知到的問題,0 表示其他原因,例如誤購 |
| price, currency | 金額(單位為 milliunits)及其 ISO 4217 貨幣代碼,便於你合計退回的金額 |
| appAccountToken | 你在購買時附加的 UUID,把退款對映回你自己使用者最乾淨的辦法 |
revision 權杖就是你不必反覆重讀整份清單的辦法
使用這個端點最笨的方法,是每次都查一位客戶並把每一頁都走一遍。這能用,但對一位有五十筆退款的客戶來說,那就是五十列你早已知道的記錄,外加新增的那一列。revision 權杖的存在就是為了消除這種浪費。每個回應都帶一個 revision。當 hasMore 為 true 時,你把它傳回去以取得下一頁。當你走到末尾時,就保留你看到的最後一個 revision。
這個端點不會做什麼
在你據此動手之前,有一個預期要先放下。Get Refund History 是按客戶維度,而非按 App 維度。你無法向它索要你 App 上週收到的每一筆退款。它只回答一個問題,即這個帳戶有哪些退款,而且你必須帶著該帳戶的一筆交易 id 才能發問。開發者們不斷撞上這堵牆,然後去找一個根本不存在的全 App 退款端點。
全 App 視圖在別處。你的 App Store Server Notifications 流會在 Apple 核准每一筆退款的那一刻發出一條 REFUND 通知,而 Get Notification History 讓你在一個日期範圍內、按退款類型過濾地重播那條流。所以分工很清晰。通知及其歷史給你全 App 的即時流。Get Refund History 按需給你一個帳戶的權威清單,這正是你在客服台或當機之後想要的。

一筆漏掉的退款要花你多少錢
端點是管路。帳單才是你鋪這根管子的理由。那份清單裡的每一筆退款都是已經退回的錢,而唯一還在你掌控裡的變數,就是你還要為一個不再付費的帳戶繼續支出多久。
你在繼續付費,去服務一個已退款的帳戶
Apple 核准退款的那一刻,購買款項就沒了。還在運轉的是交付成本。對於一個真正為每位使用者做事的 App,那就是運算、模型 API 呼叫、儲存,以及任何與其用量綁定的創作者或合作方分潤。一個你從未切斷存取的已退款客戶,就是一份你自掏腰包資助的訂閱。以 Get Refund History 做對帳,並依你查到的結果撤銷權限,就是在通知漏過去時把這個計費表關掉的辦法。
退款原因為 1 是一份喬裝的缺陷報告
如果你忽視 revocationReason,它會讓你付兩次代價。第一次代價是退款本身。第二次是同一原因帶來的每一筆未來退款。當一款產品不斷帶著 revocationReason 1 回來,也就是你 App 中一個實際或感知到的問題,Apple 就是在把一份帶標籤的樣本遞給你,告訴你到底是什麼讓客戶來要回他們的錢。按產品做趨勢分析,你就能把這個漏洞補上,而不是一筆一筆地把它賠出去。
晚一點抓到,也強過壓根沒抓到
拒付在銀行那邊是終局,而且在另一家商店,如今還帶著一筆由開發者承擔的手續費。App Store 退款不是那樣。它已經結清,但權益歸你,你一旦知道就能撤銷。所以即便是一筆你透過這個端點晚幾天才發現的退款,也值得去發現。你要不回那筆錢,但你能止住它背後仍在跑著的支出。
這如何與通知、以及與 Google 契合
把這些部件當作一個系統來看。REFUND 通知是即時訊號,隨著 Apple 的裁定推送到你的伺服器。Get Refund History 是針對單個客戶、以拉取為基礎的事實來源,是當推送失敗、或當有人需要把完整帳戶擺在面前時你會發起的呼叫。在 Google Play 那邊,形態是同一個思路、換了名字:一個 VoidedPurchaseNotification 即時推送,而 Voided Purchases API 是你拉取的那份清單。兩家商店都給你一條流和一本帳。錯誤在於只信那條流,因為流會掉。
用 RefundHalt 的方式把它接起來
一旦每個部件都就位,這個迴圈就很小。以一條 REFUND 通知作為觸發。用 Get Refund History 做對帳,好讓一個掉了的 webhook 永遠不會留下一個還活著的已退款帳戶。解碼每一筆交易,用 appAccountToken 或 transactionId 把它回連到你的使用者,讀 revocationReason,好讓一筆缺陷退款被標記出來而不只是被歸檔,並撤銷確切的那份權益,而不是整個帳戶。用 revision 權杖翻頁,這樣你讀到的是新增退款,而不是舊的。
這一部分就是 RefundHalt 替你執行的。它監聽退款通知,在需要權威清單時回落到 Get Refund History,驗證每一筆簽章交易,撤銷那筆精確的購買,並保存 revision,讓每一次遍歷都只讀發生了變化的部分。你能在數秒內切斷存取,並得到一份乾淨的記錄,說清誰被退了款、退的是什麼、原因為何,而無需自己搭起輪詢與 JWS 驗證。
常見問題解答
- 我怎麼才能看到整個 App 的每一筆退款,而不只是某一位客戶的?
- 用 Get Refund History 做不到,因為它是按客戶維度,且需要你要查詢帳戶的一筆交易 id。要拿全 App 視圖,請用你的 App Store Server Notifications 流,它會在 Apple 核准每一筆退款時發出一條 REFUND 通知;再用 Get Notification History,在一個日期範圍內按退款類型過濾地重播那條流。
- Get Refund History 端點一次回傳多少筆退款?
- 目前版本每頁最多回傳 20 筆已退款交易,按最早退款在前排序,並在 hasMore 為 true 時用 revision 權杖翻頁讀取其餘部分。已淘汰的 V1 端點在單次回應中最多回傳 50 筆。總數沒有上限,所以歷史很長的客戶只是會跨更多頁而已。
- revision 權杖是做什麼用的?
- 它既是你翻頁的辦法,也是你避免每次都重讀某位客戶整段歷史的辦法。每個回應都包含一個 revision。你把它傳回去以取得下一頁,並保存最後一個,好讓你下次查詢只回傳比該時間點更新的退款。這就把一次定時對帳限制在一小份新增記錄之內。
- 已退款交易裡的 revocationReason 是什麼意思?
- 它是 Apple 退款該交易的原因。值為 1 表示客戶是因你 App 內一個實際或感知到的問題而退款,0 則表示其他原因,例如誤購。revocationDate 告訴你退款發生的時間,以 UNIX 毫秒為單位。讀 revocationReason 能讓你把產品缺陷與一次性的後悔退款區分開。
- 如果我已經處理 REFUND 通知,還需要它嗎?
- 需要,作為兜底。通知是即時訊號,但一次推送可能在當機、一次糟糕的部署或一次 webhook 變更期間未能送達,而一筆漏掉的退款會留下一個還活著、還在花你錢的已退款帳戶。Get Refund History 是你據以對帳、以拉取為基礎的事實來源,好讓任何被 Apple 退過款的東西都不會還開著。
資料來源與延伸閱讀
- Apple Developer: Get Refund History (App Store Server API)
- Apple Developer: RefundHistoryResponse
- Apple Developer: JWSTransactionDecodedPayload (revocationDate, revocationReason)
- Apple Developer: Get Refund History V1 (deprecated)
- Apple Developer: App Store Server Notifications V2 notificationType (REFUND)
- Apple Developer: Support customers and handle refunds (WWDC21)
RefundHalt
App Store 與 Google Play 的退款自動駕駛
繼續閱讀
你的 App 可以直接彈出應用內退款申請表,客戶點擊提交後 Apple 會這樣處理
Apple 的應用內退款申請讓客戶無需離開你的 App 就能申請退款,表單由 Apple 建立並審核。本文講清 beginRefundRequest 回傳什麼、它會在你的伺服器上啟動哪些 CONSUMPTION_REQUEST 和 48 小時計時,以及這個按鈕是否值得上線。
當 Google Play 的購買被退款或被 chargeback 時,Voided Purchases API 就是你得知此事的方式
當購買被退款或被 chargeback 時,Google Play 會悄悄地作廢它。Voided Purchases API 就是這些訂單的清單,讓你可以撤銷存取權。這裡有每一個欄位、30 天的時間窗、會隱藏訂單的 revoke 選項,以及它的成本。