Khi một giao dịch mua trên Google Play được hoàn tiền hoặc bị chargeback, Voided Purchases API là cách bạn biết được điều đó
Google Play âm thầm hủy một giao dịch mua khi nó được hoàn tiền hoặc bị chargeback. Voided Purchases API là danh sách những đơn hàng đó, để bạn có thể thu hồi quyền truy cập. Đây là từng field, cửa sổ 30 ngày, tùy chọn revoke làm ẩn đơn hàng, và chi phí của nó.

Điểm chính
- Voided Purchases API, tức phương thức purchases.voidedpurchases.list, trả về các đơn hàng mà Google Play đã hủy, hoàn tiền hoặc chargeback, để bạn có thể xây dựng một hệ thống thu hồi cắt quyền truy cập vào thứ mà khách hàng không còn sở hữu.
- Chỉ những đơn hàng đã revoke mới xuất hiện. Một khoản hoàn tiền do nhà phát triển thực hiện mà không có tùy chọn revoke sẽ vô hình với API này, vì vậy nếu bạn muốn rút quyền truy cập, bạn phải hoàn tiền với revoke được bật.
- Cửa sổ là 30 ngày. startTime không thể cũ hơn 30 ngày trước, vì vậy một máy chủ ngừng hoạt động lâu hơn một tháng sẽ mất vĩnh viễn những đơn hàng bị hủy đó. Hãy poll theo lịch.
- voidedSource cho bạn biết ai đã hủy đơn hàng: 0 là người dùng, 1 là nhà phát triển, 2 là Google. voidedReason cho bạn biết lý do, từ 0 Other đến 7 Chargeback và 8 Unacknowledged_purchase.
- Real-time developer notifications đẩy một VoidedPurchaseNotification ngay khi một giao dịch mua bị hủy, nhưng hãy xem nó như một tín hiệu. Hãy gọi Voided Purchases API để lấy danh sách chính thức trước khi bạn thu hồi.
- Xác định các lần gia hạn đăng ký bằng orderId, không phải purchaseToken. Một purchaseToken bao trùm mọi lần gia hạn của một đăng ký, vì vậy chỉ riêng token không thể phân biệt hai lần gia hạn.
- Hạn mức là 6,000 truy vấn mỗi ngày và 30 truy vấn trong bất kỳ cửa sổ 30 giây nào, vì vậy hãy phân trang qua kết quả bằng continuation token và truy vấn theo cửa sổ thời gian, đừng bao giờ gọi một lần cho mỗi đơn hàng.
Một khoản hoàn tiền trên Google Play không gõ cửa nhà bạn. Tiền được chuyển đi, khách hàng vẫn mở ứng dụng, và trừ khi bạn đi tìm, không có gì thay đổi ở phía bạn. Voided Purchases API là nơi bạn đi tìm. Nó đưa cho bạn một danh sách các đơn hàng đã bị hủy, hoàn tiền hoặc chargeback, để bạn có thể thu hồi quyền truy cập vào thứ mà khách hàng không còn trả tiền nữa. Hướng một scheduled job vào đó, đọc danh sách, cắt entitlement. Đó là toàn bộ vòng lặp.
Có một cái bẫy khiến hầu hết các nhóm vấp phải, và nó không nằm trong code. Chỉ những đơn hàng đã được revoke mới hiển thị ở đây. Nếu bạn hoàn tiền một giao dịch mua trong Play Console mà không tích tùy chọn revoke, đơn hàng đó sẽ không bao giờ đến được API này, và job của bạn chạy trơn tru trong khi một khách hàng đã được hoàn tiền vẫn giữ mọi thứ bạn đã bán cho họ. Bài viết này đi qua API theo từng field, những con số giới hạn nó, và nơi tiền rò rỉ khi bạn bỏ qua nó.
Voided Purchases API thực sự trả về những gì
API trả lời một câu hỏi: những đơn hàng nào của ứng dụng này gần đây đã bị hủy. Một lần hủy (void) bao gồm ba kết cục đều kết thúc bằng việc khách hàng lấy lại tiền. Một lần hủy, một lần hoàn tiền, hoặc một lần chargeback. Nó áp dụng cho các sản phẩm trong ứng dụng mua một lần và cho các đăng ký, và bạn chọn phạm vi bằng một tham số duy nhất. Đặt type bằng 0 và bạn chỉ nhận được các giao dịch mua sản phẩm trong ứng dụng bị hủy, đây là mặc định. Đặt bằng 1 và bạn nhận được cả các giao dịch mua trong ứng dụng bị hủy lẫn các giao dịch mua đăng ký bị hủy.
Mỗi mục trong danh sách là một đối tượng voided purchase. Các field thì ít và mỗi field đều quan trọng.
Các field trên một voided purchase
| Field | Nó chứa gì |
|---|---|
| orderId | Order id nhận diện duy nhất một giao dịch mua một lần, một giao dịch mua đăng ký, hoặc một lần gia hạn đăng ký đơn lẻ. Đây là khóa join của bạn |
| purchaseToken | Token nhận diện một giao dịch mua một lần hoặc một đăng ký. Nó không phân biệt các lần gia hạn, vì vậy hãy dùng orderId cho việc đó |
| purchaseTimeMillis | Thời điểm giao dịch mua được thực hiện, tính bằng mili giây kể từ epoch |
| voidedTimeMillis | Thời điểm giao dịch mua bị hủy, hoàn tiền hoặc chargeback, tính bằng mili giây kể từ epoch |
| voidedSource | Ai khởi tạo việc hủy: 0 người dùng, 1 nhà phát triển, 2 Google |
| voidedReason | Lý do giao dịch mua bị hủy, một số nguyên từ 0 đến 8 |
| voidedQuantity | Số lượng bị hủy từ một khoản hoàn tiền một phần dựa trên số lượng, chỉ được trả về khi includeQuantityBasedPartialRefund là true |
Đọc voidedReason trước khi bạn hành động
voidedReason là field biến một danh sách thô thành một quyết định. Một khoản hoàn tiền do hối tiếc của người mua và một khoản chargeback từ ngân hàng đều rơi vào cùng một danh sách, nhưng chúng không phải là cùng một sự kiện, và bảng giá tháng Tám khiến một trong số chúng trở nên đắt đỏ. Đây là bộ đầy đủ.
| voidedReason | Label | Nó có ý nghĩa gì với bạn |
|---|---|---|
| 0 | Other | Không có danh mục nào được gán. Thu hồi và tiếp tục |
| 1 | Remorse | Người mua đổi ý. Một khoản hoàn tiền thông thường |
| 2 | Not_received | Khách hàng nói rằng họ chưa bao giờ nhận được sản phẩm. Đáng để kiểm tra khâu giao hàng của bạn |
| 3 | Defective | Sản phẩm không hoạt động. Một tín hiệu chất lượng, hãy ghi lại |
| 4 | Accidental_purchase | Một giao dịch mua ngoài ý muốn, thường trên thiết bị dùng chung |
| 5 | Fraud | Google đánh dấu giao dịch là gian lận |
| 6 | Friendly_fraud | Một khoản chargeback trong đó chủ thẻ hợp pháp tranh chấp một khoản phí do chính họ thực hiện |
| 7 | Chargeback | Ngân hàng của khách hàng đảo ngược khoản thanh toán. Chung cuộc với ngân hàng, và giờ bị tính vào bạn |
| 8 | Unacknowledged_purchase | Google tự động hoàn tiền một giao dịch mua mà ứng dụng của bạn chưa bao giờ acknowledge |
Cửa sổ 30 ngày là cái bẫy làm rỗng danh sách của bạn
Voided Purchases API chỉ có thể hiển thị các giao dịch mua bị hủy trong 30 ngày qua. Tham số startTime mặc định bằng thời điểm hiện tại trừ đi 30 ngày, và không thể đặt cũ hơn thế. endTime mặc định là bây giờ. Vì vậy endpoint là một cửa sổ một tháng cuộn, không phải một kho lưu trữ.
Hệ quả rất thẳng thừng. Nếu polling job của bạn hỏng và không ai nhận ra trong năm tuần, các lần hủy từ tuần đầu tiên đã hết hạn khỏi API. Không có lệnh gọi nào mang chúng trở lại. Bạn sẽ không thu hồi những đơn hàng đó, và bạn thậm chí sẽ không biết chúng từng tồn tại trừ khi bạn đã ghi lại chúng bằng cách khác. API là một tấm lưới an toàn với một cái lỗ có kích cỡ bằng lần gián đoạn tồi tệ nhất của bạn.
Tùy chọn revoke quyết định liệu một đơn hàng có xuất hiện hay không
Đây là lý do phổ biến nhất khiến một nhóm báo cáo API bị hỏng. Chỉ những đơn hàng đã revoke mới được trả về. Các khoản hoàn tiền do người dùng khởi tạo, các lần hủy, các lần chargeback, và các khoản hoàn tiền do Google khởi tạo luôn được revoke, vì vậy chúng luôn xuất hiện. Một khoản hoàn tiền do nhà phát triển khởi tạo thì khác. Khi bạn tự hoàn tiền một đơn hàng, qua Play Console hoặc Orders API, bạn chọn có revoke nó hay không. Hoàn tiền mà không revoke, và đơn hàng được giải quyết với khách hàng nhưng không bao giờ hiện ra trong Voided Purchases API.
Quy tắc theo sau rất đơn giản. Nếu ý định của bạn là rút quyền truy cập, hãy hoàn tiền với tùy chọn revoke được bật. Nếu không, bạn đã trả lại tiền và để ngỏ cửa, và revocation job của bạn, dù được viết tốt đến đâu, cũng không có gì để hành động.
Cách poll mà không vượt hạn mức
Endpoint bị giới hạn tốc độ, và các giới hạn đủ thấp để một vòng lặp ngây thơ sẽ chạm phải chúng. Bạn có 6,000 truy vấn mỗi ngày, tính theo Pacific Time, và không quá 30 truy vấn trong bất kỳ khoảng 30 giây nào. Ngân sách đó ổn cho việc poll theo cửa sổ và không thân thiện với các thiết kế một-yêu-cầu-cho-mỗi-đơn-hàng.
Các cửa sổ truy vấn và continuation token
maxResults mặc định là 1,000, cũng chính là giới hạn trên. Khi một cửa sổ chứa nhiều hơn một trang các lần hủy, phản hồi mang theo một đối tượng tokenPagination với một nextPageToken. Gửi lại token đó ở lệnh gọi tiếp theo để đi qua các trang. Đặt startTime và endTime để giới hạn cửa sổ bạn quan tâm, phân trang cho đến khi hết token, rồi tiến cửa sổ tới. Mẫu đó giữ bạn nằm trong cả giới hạn burst 30 giây lẫn hạn mức hằng ngày.
Real-time developer notifications lấp đầy khoảng trống
Poll mỗi ngày vẫn để lại tối đa một ngày mù, và cửa sổ 30 ngày trừng phạt những khoảng trống dài. Real-time developer notifications loại bỏ độ trễ đó. Google phát hành một VoidedPurchaseNotification tới một chủ đề Cloud Pub/Sub mà bạn sở hữu ngay khi một giao dịch mua bị hủy, và backend của bạn tiêu thụ nó trong vài giây. Thông điệp thì nhỏ.
| RTDN field | Nó chứa gì |
|---|---|
| purchaseToken | Token từ giao dịch mua ban đầu |
| orderId | Order id cho giao dịch bị hủy, một cái mới cho mỗi lần gia hạn đăng ký |
| productType | 1 cho một đăng ký, 2 cho một giao dịch mua một lần |
| refundType | 1 cho hoàn tiền toàn phần, 2 cho hoàn tiền một phần dựa trên số lượng |

Điều này khiến bạn tốn bao nhiêu tiền
API là hệ thống ống dẫn, nhưng lý do để lắp đặt nó là một hóa đơn. Mỗi lần hủy trong danh sách đó ánh xạ tới một con số thật, và hai trong số chúng đang trở nên đắt hơn.
Hóa đơn chargeback rơi vào bạn từ ngày 3 tháng 8 năm 2026
Bắt đầu từ ngày 3 tháng 8 năm 2026, Google chuyển chi phí của một khoản chargeback sang nhà phát triển. Bạn mất giá mua và bạn phải trả thêm phí chargeback của ngân hàng. Một voidedReason bằng 7 không còn chỉ là một giao dịch bán bị mất, nó là một mục có kèm phí. Bạn không thể đảo ngược một khoản chargeback, nó chung cuộc với ngân hàng, nhưng bạn có thể cầm máu sau đó. Bắt kịp lần hủy nhanh cho phép bạn thu hồi entitlement và, đối với bất cứ thứ gì bạn vẫn đang cung cấp, ngừng chi tiêu cho một khách hàng đã được hoàn tiền rồi bị đảo ngược.
Bạn vẫn tiếp tục trả tiền để phục vụ một khách hàng đã được hoàn tiền
Giá mua biến mất ngay khi một lần hủy xuất hiện. Thứ bạn vẫn kiểm soát là chi phí tiếp tục cung cấp. Mỗi giờ một entitlement đã được hoàn tiền còn hoạt động, bạn vẫn trả tiền cho những thứ mà khách hàng không còn tài trợ nữa: điện toán, các lệnh gọi API mô hình, lưu trữ, và bất kỳ khoản chi trả cho nhà sáng tạo hay đối tác nào gắn với việc sử dụng của họ. Một hệ thống thu hồi được vận hành bởi API này là cách bạn tắt cái đồng hồ đo đó. Bỏ qua nó, và bạn tài trợ cho sản phẩm dành cho những người mà cửa hàng đã bồi hoàn xong.
Friendly fraud là một mẫu hình đáng để theo dõi xu hướng
Một voidedReason bằng 5 hoặc 6 không phải là chuyện xảy ra một lần. Fraud và friendly fraud kết cụm theo tài khoản, theo thiết bị, và đôi khi theo chương trình khuyến mãi. API cung cấp cho bạn voidedSource và voidedReason trên mỗi lần hủy, đủ để theo dõi xu hướng lạm dụng theo tài khoản thay vì coi mỗi lần đảo ngược như một chi phí riêng lẻ. Một khách hàng chargeback hai lần đang nói với bạn điều gì đó mà lần hoàn tiền đầu tiên không nói.
Lắp đặt theo cách của RefundHalt
Mô hình rất nhỏ một khi bạn nắm được tất cả các mảnh ghép. Lắng nghe VoidedPurchaseNotification theo thời gian thực để không có gì phải chờ trọn một ngày. Gọi Voided Purchases API làm nguồn sự thật, khóa theo orderId để các lần gia hạn đăng ký không bao giờ bị nhầm lẫn. Đọc voidedSource và voidedReason để một khoản chargeback được xử lý khác với một khoản hoàn tiền do hối tiếc. Poll theo một lịch đủ chặt để cửa sổ 30 ngày không bao giờ cắn, và hoàn tiền với tùy chọn revoke được bật bất cứ khi nào ý định của bạn là cắt quyền truy cập.
Đây là phần RefundHalt chạy thay cho bạn. Nó tiêu thụ các thông báo thời gian thực, đối soát mọi lần hủy với API, thu hồi đúng đơn hàng thay vì toàn bộ sản phẩm, và tách một khoản chargeback ngân hàng khỏi một khoản hoàn tiền thông thường để những khoản đắt đỏ được đánh dấu, không bị chôn vùi. Bạn được thu hồi quyền truy cập trong vài giây và một bản ghi về việc ai đã hủy cái gì và tại sao, mà không phải tự dựng lên một pipeline Pub/Sub và một polling job.
Câu hỏi thường gặp
- Tại sao các đơn hàng đã hoàn tiền của tôi không hiển thị trong Voided Purchases API?
- Bởi vì chỉ những đơn hàng đã revoke mới được trả về. Các khoản hoàn tiền của người dùng, các lần hủy, các lần chargeback, và các khoản hoàn tiền do Google khởi tạo luôn được revoke và luôn xuất hiện. Một khoản hoàn tiền do nhà phát triển khởi tạo chỉ xuất hiện nếu bạn cũng đã chọn tùy chọn revoke. Nếu bạn hoàn tiền một đơn hàng mà không revoke nó, đơn hàng được giải quyết nhưng vô hình với API này, vì vậy hãy hoàn tiền với revoke được bật bất cứ khi nào bạn có ý định rút quyền truy cập.
- Voided Purchases API truy ngược lại được bao xa?
- Ba mươi ngày. Tham số startTime mặc định bằng thời điểm hiện tại trừ đi 30 ngày và không thể đặt cũ hơn thế, vì vậy endpoint là một cửa sổ một tháng cuộn thay vì một kho lưu trữ. Một đơn hàng bị hủy đã quá 30 ngày sẽ biến mất khỏi API mà không có cách nào lấy lại, đó là lý do bạn poll theo lịch và bổ trợ nó bằng các thông báo thời gian thực.
- Tôi nên dùng real-time developer notifications hay Voided Purchases API để thu hồi quyền truy cập?
- Hãy dùng cả hai. VoidedPurchaseNotification đến trong vài giây và bảo bạn hãy nhìn, nhưng chính hướng dẫn của Google là xem nó như một tín hiệu, không phải một nguồn sự thật. Hãy gọi Voided Purchases API để xác nhận trạng thái hiện tại, rồi thu hồi. Thông báo loại bỏ độ trễ, và API cung cấp cho bạn voidedSource và voidedReason chính thức để hành động.
- Làm thế nào để phân biệt một khoản chargeback với một khoản hoàn tiền thông thường trong API?
- Hãy đọc field voidedReason. Giá trị 7 là một khoản chargeback, nghĩa là ngân hàng của khách hàng đã đảo ngược khoản thanh toán, và 6 là friendly fraud. Giá trị 1 là một khoản hoàn tiền do hối tiếc. Điều này quan trọng vì từ ngày 3 tháng 8 năm 2026 Google chuyển giá mua chargeback và phí ngân hàng sang nhà phát triển, nên một voidedReason bằng 7 khiến bạn tốn kém hơn một khoản hoàn tiền thông thường.
- Voided Purchases API có bao gồm các đăng ký không?
- Có. Đặt tham số type bằng 1 để nhận được cả các giao dịch mua trong ứng dụng bị hủy lẫn các giao dịch mua đăng ký bị hủy. Mặc định, type 0, chỉ trả về các giao dịch mua sản phẩm trong ứng dụng. Đối với các đăng ký, hãy xác định đúng kỳ bị hủy bằng orderId, vì một purchaseToken bao trùm mọi lần gia hạn và một orderId mới được tạo cho mỗi giao dịch gia hạn.
Nguồn và tài liệu đọc thêm
- 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
Chế độ tự động xử lý hoàn tiền cho App Store và Google Play
Đọc tiếp
Ba thông báo hoàn tiền App Store đến sau khi Apple quyết định, và REFUND_REVERSED trả lại doanh số cho bạn
Apple gửi bốn tin nhắn hoàn tiền qua App Store Server Notifications V2, và hầu hết ứng dụng chỉ xử lý hai. REFUND yêu cầu bạn thu hồi, REFUND_DECLINED nghĩa là giữ doanh số, và REFUND_REVERSED trả lại doanh số và yêu cầu bạn khôi phục những gì đã lấy đi. Đây là những gì mỗi loại đòi hỏi.
Mọi yêu cầu hoàn tiền của Apple giờ đều kèm theo lý do, và consumptionRequestReason là cách bạn đọc nó
Kể từ WWDC24, mỗi CONSUMPTION_REQUEST của Apple đều mang theo consumptionRequestReason, lý do do chính khách hàng nêu ra khi muốn hoàn tiền. Có năm giá trị, từ UNINTENDED_PURCHASE đến LEGAL, và mỗi giá trị nên thay đổi những gì bạn gửi lại trong khung thời gian 12 giờ của mình. Đây là cách đọc từng giá trị.