當 Google Play 的購買被退款或被 chargeback 時,Voided Purchases API 就是你得知此事的方式
當購買被退款或被 chargeback 時,Google Play 會悄悄地作廢它。Voided Purchases API 就是這些訂單的清單,讓你可以撤銷存取權。這裡有每一個欄位、30 天的時間窗、會隱藏訂單的 revoke 選項,以及它的成本。

重點摘要
- Voided Purchases API,也就是 purchases.voidedpurchases.list 方法,會回傳 Google Play 已取消、退款或 chargeback 的訂單,讓你可以建立一套撤銷系統,切斷客戶不再擁有的東西的存取權。
- 只有被 revoke 的訂單才會出現。開發者在未使用 revoke 選項的情況下發出的退款,對這個 API 是看不見的,所以如果你想收回存取權,就必須在開啟 revoke 的情況下退款。
- 時間窗是 30 天。startTime 不能早於 30 天前,所以停機超過一個月的伺服器會永久失去那些作廢的訂單。要按排程輪詢。
- voidedSource 告訴你是誰作廢了訂單:0 是使用者,1 是開發者,2 是 Google。voidedReason 告訴你原因,從 0 Other 到 7 Chargeback 以及 8 Unacknowledged_purchase。
- Real-time developer notifications 會在購買被作廢的那一刻推送一個 VoidedPurchaseNotification,但要把它當成一個訊號。在撤銷之前,呼叫 Voided Purchases API 取得權威的清單。
- 用 orderId 而不是 purchaseToken 來辨識訂閱的續訂。一個 purchaseToken 涵蓋一項訂閱的每一次續訂,所以單靠 token 無法分辨兩次續訂。
- 配額是每天 6,000 次查詢,以及任何 30 秒視窗內 30 次查詢,所以要用 continuation token 逐頁翻閱結果,並按時間窗查詢,絕不要一筆訂單一次呼叫。
Google Play 上的退款不會來敲你的門。錢款移動了,客戶還開著 App,除非你去找,否則你這邊什麼都不會改變。Voided Purchases API 就是你去找的地方。它交給你一份已取消、退款或 chargeback 的訂單清單,讓你可以撤銷客戶不再付費之物的存取權。把一個排程工作指向它,讀取清單,切斷權限。這就是整個循環。
有一個陷阱會絆倒大多數團隊,而且它不在程式碼裡。只有被 revoke 的訂單才會在這裡出現。如果你在 Play Console 退款一筆購買卻沒有勾選 revoke 選項,那筆訂單永遠不會到達這個 API,而你的工作會乾淨地執行,同時一個已退款的客戶保留了你賣給他的一切。這篇文章逐一欄位走過這個 API、界定它的數字,以及當你略過它時錢從哪裡漏出去。
Voided Purchases API 實際上回傳什麼
這個 API 回答一個問題:這個 App 的哪些訂單最近被作廢了。作廢涵蓋三種都以客戶拿回錢款告終的結果。取消、退款或 chargeback。它適用於一次性的 App 內產品和訂閱,而你用單一參數選擇範圍。把 type 設為 0,你就只會得到被作廢的 App 內產品購買,這是預設值。把它設為 1,你就會同時得到被作廢的 App 內購買和被作廢的訂閱購買。
清單中的每一筆項目都是一個被作廢的購買物件。欄位不多,而且每一個都重要。
被作廢購買上的欄位
| 欄位 | 它存放什麼 |
|---|---|
| orderId | 唯一辨識一筆一次性購買、一筆訂閱購買,或單一次訂閱續訂的訂單 id。這是你的 join 鍵 |
| purchaseToken | 辨識一筆一次性購買或一項訂閱的 token。它不區分續訂,所以那種情況請用 orderId |
| purchaseTimeMillis | 購買發生的時間,以自 epoch 起的毫秒計 |
| voidedTimeMillis | 購買被取消、退款或 chargeback 的時間,以自 epoch 起的毫秒計 |
| voidedSource | 誰發起了作廢:0 使用者,1 開發者,2 Google |
| voidedReason | 購買被作廢的原因,一個 0 到 8 的整數 |
| voidedQuantity | 來自以數量為基礎的部分退款的作廢數量,只有當 includeQuantityBasedPartialRefund 為 true 時才會回傳 |
在採取行動之前先讀 voidedReason
voidedReason 是把原始清單轉成決策的欄位。買家後悔的退款和銀行的 chargeback 都落在同一份清單裡,但它們不是同一種事件,而八月的定價讓其中一種變得昂貴。以下是完整的一組。
| voidedReason | Label | 它對你意味著什麼 |
|---|---|---|
| 0 | Other | 未指派任何類別。撤銷然後繼續 |
| 1 | Remorse | 買家改變了主意。一筆普通的退款 |
| 2 | Not_received | 客戶說他從未收到產品。值得檢查你的交付 |
| 3 | Defective | 產品無法運作。一個品質訊號,記錄下來 |
| 4 | Accidental_purchase | 一筆無意的購買,常見於共用裝置 |
| 5 | Fraud | Google 將這筆交易標記為詐欺 |
| 6 | Friendly_fraud | 一筆 chargeback,其中合法的持卡人爭議一筆他自己做的扣款 |
| 7 | Chargeback | 客戶的銀行撤銷了付款。與銀行是最終定案,而且現在向你收費 |
| 8 | Unacknowledged_purchase | Google 自動退款了一筆你的 App 從未確認(acknowledge)的購買 |
30 天的時間窗是清空你清單的陷阱
Voided Purchases API 只能顯示過去 30 天內被作廢的購買。startTime 參數預設為目前時間減 30 天,而且不能設得比那更早。endTime 預設為現在。所以這個端點是一個滾動的一個月時間窗,而不是一個封存檔案。
後果很直接。如果你的輪詢工作壞了而五週內沒有人注意到,第一週的作廢就已經因逾期而離開了 API。沒有任何呼叫能把它們帶回來。你不會撤銷那些訂單,而且除非你用其他方式擷取過它們,否則你甚至不會知道它們曾經存在。這個 API 是一張安全網,上面有一個跟你最糟糕的停機一樣大的洞。
revoke 選項決定一筆訂單是否會出現
這是團隊回報 API 壞掉的最常見原因。只有被 revoke 的訂單才會被回傳。使用者發起的退款、取消、chargeback 和 Google 發起的退款總是會被 revoke,所以它們總是會出現。開發者發起的退款則不同。當你自己退款一筆訂單,透過 Play Console 或 Orders API 時,你選擇是否也要 revoke 它。退款而不 revoke,那筆訂單就與客戶結清了,但永遠不會浮現在 Voided Purchases API 裡。
隨之而來的規則很簡單。如果你的意圖是收回存取權,就在開啟 revoke 選項的情況下退款。否則你已經退了錢卻讓門開著,而你的撤銷工作,無論寫得多好,都沒有東西可以處理。
如何在不觸及配額的情況下輪詢它
這個端點有速率限制,而限制低到一個天真的迴圈就會撞上它們。你每天有 6,000 次查詢,以太平洋時間計算,任何 30 秒期間內不超過 30 次查詢。這個預算對於視窗式輪詢綽綽有餘,卻對一筆訂單一次請求的設計充滿敵意。
查詢視窗與 continuation token
maxResults 預設為 1,000,這也是上限。當一個視窗容納超過一頁的作廢時,回應會帶著一個含有 nextPageToken 的 tokenPagination 物件。在下一次呼叫時把那個 token 傳回去,以走過各頁。設定 startTime 和 endTime 來界定你在意的視窗,翻頁直到 token 用完,然後推進視窗。這個模式讓你同時待在 30 秒的爆量限制和每日上限之內。
Real-time developer notifications 填補了空隙
每天輪詢仍會留下最多一天的盲區,而 30 天的時間窗會懲罰長間隔。Real-time developer notifications 移除了延遲。Google 會在購買被作廢的那一刻,把一個 VoidedPurchaseNotification 發佈到你擁有的 Cloud Pub/Sub topic,而你的後端會在幾秒內消費它。這則訊息很小。
| RTDN 欄位 | 它存放什麼 |
|---|---|
| purchaseToken | 來自原始購買的 token |
| orderId | 被作廢交易的訂單 id,每一次訂閱續訂都是一個新的 |
| productType | 1 代表訂閱,2 代表一次性購買 |
| refundType | 1 代表全額退款,2 代表以數量為基礎的部分退款 |

這在金錢上讓你付出什麼代價
這個 API 是水電管線,但接上它的理由是一張帳單。那份清單裡的每一筆作廢都對應到一個真實的數字,而其中兩個正變得更昂貴。
從 2026 年 8 月 3 日起 chargeback 的帳單落到你身上
從 2026 年 8 月 3 日起,Google 把 chargeback 的成本轉嫁到開發者身上。你失去購買價金,還要在上面付銀行的 chargeback 手續費。voidedReason 為 7 不再只是一筆流失的銷售,它是一個附帶手續費的支出項目。你無法逆轉一筆 chargeback,它與銀行是最終定案,但你可以在它之後止血。快速抓到作廢讓你可以撤銷權限,而且對於你仍在交付的任何東西,停止在一個先退款又被撤銷付款的客戶身上花錢。
你持續付費去服務一個已退款的客戶
作廢一出現,購買價金就在那一刻消失了。你仍能控制的是持續交付的成本。一個已退款的權限每多活一個小時,你就持續為客戶不再資助的東西付費:運算、模型 API 呼叫、儲存,以及任何綁在他使用量上的創作者或夥伴分潤。一套由這個 API 驅動的撤銷系統,就是你關掉那個計量表的方式。略過它,你就是在為商店已經補償過的人資助這個產品。
Friendly fraud 是一個值得追蹤趨勢的模式
voidedReason 為 5 或 6 不是一次性的事件。Fraud 和 friendly fraud 會按帳號、按裝置,有時按促銷活動群聚。API 在每一筆作廢上給你 voidedSource 和 voidedReason,這足以按帳號追蹤濫用的趨勢,而不是把每一次撤銷付款當成一項孤立的成本。一個做了兩次 chargeback 的客戶,正告訴你第一次退款沒有告訴你的事。
用 RefundHalt 的方式把它接起來
一旦你握住所有的零件,這個模型就很小。即時聆聽 VoidedPurchaseNotification,好讓任何東西都不必等上整整一天。呼叫 Voided Purchases API 作為事實的來源,以 orderId 為鍵,好讓訂閱續訂永遠不會被搞混。讀取 voidedSource 和 voidedReason,好讓 chargeback 的處理不同於後悔退款。按一個緊到 30 天時間窗永遠不會咬到你的排程輪詢,並在你的意圖是切斷存取權時,一律在開啟 revoke 選項的情況下退款。
這就是 RefundHalt 為你執行的部分。它消費即時通知,把每一筆作廢與 API 對帳,撤銷確切的那筆訂單而不是整個產品,並把銀行 chargeback 與一筆普通退款分開,好讓昂貴的那些被標記出來,而不是被埋沒。你在幾秒內就取得被撤銷的存取權,以及一份誰作廢了什麼、為什麼的紀錄,而不必自己架起一條 Pub/Sub 管線和一個輪詢工作。
常見問題解答
- 為什麼我的已退款訂單沒有出現在 Voided Purchases API 裡?
- 因為只有被 revoke 的訂單才會被回傳。使用者退款、取消、chargeback 和 Google 發起的退款總是會被 revoke 且總是會出現。開發者發起的退款只有在你也選了 revoke 選項時才會出現。如果你退款一筆訂單卻沒有 revoke 它,那筆訂單就結清了但對這個 API 是看不見的,所以每當你打算收回存取權時,就在開啟 revoke 的情況下退款。
- Voided Purchases API 能回溯多久?
- 三十天。startTime 參數預設為目前時間減 30 天,而且不能設得比那更早,所以這個端點是一個滾動的一個月時間窗,而不是一個封存檔案。一筆逾 30 天而過期的作廢訂單會從 API 中消失,沒有任何辦法取回它,這就是為什麼你要按排程輪詢,並以 real-time notifications 作為後盾。
- 我應該用 real-time developer notifications 還是 Voided Purchases API 來撤銷存取權?
- 兩者都用。VoidedPurchaseNotification 在幾秒內抵達並告訴你去看,但 Google 自己的指引是把它當成一個訊號,而不是事實的來源。呼叫 Voided Purchases API 以確認目前的狀態,然後撤銷。通知移除了延遲,而 API 給你權威的 voidedSource 和 voidedReason 以供採取行動。
- 我要如何在 API 裡分辨 chargeback 和一筆普通退款?
- 讀取 voidedReason 欄位。值為 7 是 chargeback,意思是客戶的銀行撤銷了付款,而 6 是 friendly fraud。值為 1 是一筆後悔退款。這很重要,因為從 2026 年 8 月 3 日起,Google 把 chargeback 的購買價金和銀行手續費轉給開發者,所以 voidedReason 為 7 讓你付出的比一筆普通退款更多。
- Voided Purchases API 涵蓋訂閱嗎?
- 涵蓋。把 type 參數設為 1,就能同時取得被作廢的 App 內購買和被作廢的訂閱購買。預設值 type 0 只回傳 App 內產品購買。對於訂閱,用 orderId 辨識確切的被作廢週期,因為一個 purchaseToken 涵蓋每一次續訂,而每一筆續訂交易都會產生一個新的 orderId。
資料來源與延伸閱讀
- Google Play Developer API: Voided Purchases API guide
- Google Play Developer API: purchases.voidedpurchases.list method
- Google Play Developer API: purchases.voidedpurchases resource (voidedSource and voidedReason)
- Android Developers: Real-time developer notifications reference (VoidedPurchaseNotification)
- Android Developers: Fight fraud and abuse with Play Billing
RefundHalt
App Store 與 Google Play 的退款自動駕駛
繼續閱讀
過多的退款申請,代價可能遠超退款本身,因為兩大 App 商店都把它視為品質訊號
Apple 審查準則 5.6.4 把過多的退款申請列為 App 未達品質標準的跡象,Google 的開發人員發布協議也允許它依退款率下架新開發人員的 App。本文整理兩家商店的原文、下架的實際成本,以及如何避開風險。
在處理退款之前先驗證 App Store 伺服器通知,因為任何人都能向你的 webhook URL 發送請求
在檢查簽章之前,一則退款通知只不過是一個 HTTPS 請求。本文說明如何驗證 App Store 伺服器通知和 Google Play RTDN 訊息、各商店簽署的內容,以及偽造的退款或遭錯誤拒絕的退款會讓你付出什麼代價。