Chuyển đến nội dung
Google Play Refund Management

Hoàn tiền trên Google Play sẽ không cho bạn biết, nhưng Voided Purchases API thì có

Một khoản hoàn tiền trên Google Play diễn ra âm thầm: giao dịch bị đảo ngược, khách hàng vẫn tiếp tục dùng ứng dụng, và không có gì thay đổi ở phía bạn trừ khi bạn kiểm tra. Voided Purchases API chính là nơi bạn kiểm tra. Dưới đây là API này được phân tích từng trường dữ liệu, các giới hạn bao quanh nó, và nơi tiền âm thầm thất thoát khi bạn chưa kết nối nó.

5 min read
Hoàn tiền trên Google Play sẽ không cho bạn biết, nhưng Voided Purchases API thì có

Một khoản hoàn tiền trên Google Play diễn ra âm thầm. Giao dịch bị đảo ngược, khách hàng vẫn tiếp tục dùng ứng dụng, và không có gì thay đổi ở phía bạn trừ khi bạn chủ động kiểm tra. Kiểm tra chính là mục đích của Voided Purchases API. Nó trả về 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 bất cứ thứ gì khách hàng đã ngừng thanh toán. Kết nối một tác vụ định kỳ (scheduled job) với nó, đọc dữ liệu trả về, và revoke quyền sử dụng (entitlement). Đó là toàn bộ cơ chế.

Có một điều khiến hầu như mọi đội ngũ đều mắc bẫy, và nó nằm ở chính sách chứ không phải ở code. API này chỉ liệt kê những đơn hàng thực sự đã bị revoke. Nếu bạn hoàn tiền một giao dịch mua trong Play Console mà không tick chọn tùy chọn revoke, đơn hàng đó sẽ không bao giờ xuất hiện ở đây, vì vậy tác vụ của bạn vẫn chạy sạch sẽ hoàn hảo trong khi một khách hàng đã được hoàn tiền vẫn ung dung giữ nguyên mọi thứ bạn đã bán. Phần tiếp theo sẽ đi qua API này từng trường dữ liệu, các giới hạn bao quanh nó, và nơi tiền âm thầm thất thoát khi nó chưa được kết nối.

Những điểm chính

  • Voided Purchases API, được truy cập qua phương thức purchases.voidedpurchases.list, liệt kê các đơn hàng mà Google Play đã hủy, hoàn tiền hoặc chargeback, đây chính là điều cho phép bạn xây dựng một hệ thống tước bỏ quyền truy cập vào những giao dịch mua mà khách hàng không còn sở hữu.

  • Chỉ những đơn hàng đã bị revoke mới hiển thị. Một khoản hoàn tiền do nhà phát triển thực hiện mà không revoke sẽ vẫn vô hình ở đây, vì vậy muốn thu hồi quyền truy cập nghĩa là phải hoàn tiền kèm bật revoke.

  • Phạm vi truy xuất của nó là một cửa sổ trượt 30 ngày. Vì startTime không thể lùi xa hơn 30 ngày, bất kỳ server nào offline lâu hơn một tháng sẽ mất vĩnh viễn các void đó, đó là lý do polling phải chạy theo lịch trình.

  • voidedSource xác định ai là người kích hoạt void: 0 là người dùng, 1 là nhà phát triển, 2 là Google. voidedReason giải thích lý do, chạy từ 0 cho Other cho đến 7 cho Chargeback và 8 cho Unacknowledged_purchase.

  • Real-time developer notifications sẽ phát ra một VoidedPurchaseNotification ngay khi một giao dịch mua bị voided, nhưng hãy xem đó chỉ là một lời nhắc. Hãy xác nhận lại với Voided Purchases API trước khi revoke bất cứ thứ gì.

  • Hãy phân biệt các lần gia hạn subscription bằng orderId, không bao giờ bằng purchaseToken. Một purchaseToken duy nhất trải dài qua mọi lần gia hạn của một subscription, vì vậy bản thân token không thể tách biệt kỳ này với kỳ khác.

  • Giới hạn tối đa là 6,000 truy vấn mỗi ngày và 30 truy vấn trong bất kỳ khoảng 30 giây nào, vì vậy hãy giới hạn mỗi request theo một khung thời gian và phân trang qua nó bằng continuation token, không bao giờ gửi một request cho mỗi đơn hàng.

Những gì API trả về

Endpoint này giải quyết một câu hỏi duy nhất: những đơn hàng nào của ứng dụng này gần đây đã bị voided. Một void gộp ba kết quả đều trả lại tiền cho khách hàng, cụ thể là hủy, hoàn tiền, hoặc chargeback. Nó bao phủ cả các sản phẩm in-app mua một lần lẫn subscription, và một tham số sẽ quyết định phạm vi này. Để type ở giá trị mặc định là 0, bạn chỉ nhận lại các giao dịch mua sản phẩm in-app bị voided. Đặt type thành 1, bạn sẽ nhận được cả các giao dịch mua in-app bị voided lẫn các giao dịch subscription bị voided.

Mỗi mục trong response là một voided purchase object với một tập hợp các trường dữ liệu ngắn gọn nhưng có giá trị cao:

  • orderId đánh dấu duy nhất một giao dịch mua một lần, một subscription, hoặc một lần gia hạn riêng lẻ bên trong nó. Hãy coi đây là join key của bạn.

  • purchaseToken xác định một giao dịch mua một lần hoặc một subscription, nhưng nó không phân biệt được các lần gia hạn, vì vậy hãy dựa vào orderId cho việc đó.

  • purchaseTimeMillis ghi lại thời điểm giao dịch mua diễn ra, tính bằng mili giây kể từ epoch.

  • voidedTimeMillis ghi lại thời điểm nó bị hủy, hoàn tiền hoặc chargeback, cũng tính bằng mili giây kể từ epoch.

  • voidedSource cho biết ai là người bắt đầu void, trong đó 0 là người dùng, 1 là nhà phát triển, và 2 là Google.

  • voidedReason cho biết nguyên nhân dưới dạng một số nguyên từ 0 đến 8.

  • voidedQuantity mang số lượng bị void từ một khoản hoàn tiền một phần dựa trên số lượng (quantity-based partial refund), và chỉ xuất hiện khi includeQuantityBasedPartialRefund là true.

Vì một purchaseToken bao phủ toàn bộ một subscription trong khi mỗi lần gia hạn lại có một orderId hoàn toàn mới, việc dùng token làm khóa cho các entitlement của bạn sẽ khiến bạn revoke nhầm kỳ. Hãy dùng orderId làm khóa thay vào đó.

Đọc voidedReason trước khi động vào bất cứ thứ gì

voidedReason chính là thứ biến một danh sách đơn thuần thành một quyết định thực sự, bởi vì một khoản hoàn tiền do đổi ý và một chargeback từ ngân hàng cùng nằm trong một feed nhưng lại hoàn toàn khác nhau. Toàn bộ tập giá trị như sau:

1. Other không mang một danh mục được gán cụ thể. Cứ revoke rồi tiếp tục.

2. Remorse là một khách hàng đã đổi ý, một khoản hoàn tiền thông thường hàng ngày.

3. Not_received là khiếu nại rằng sản phẩm chưa bao giờ được nhận, đáng để bạn xem lại khâu giao hàng.

4. Defective nghĩa là sản phẩm không hoạt động, một tín hiệu về chất lượng mà bạn nên ghi lại.

5. Accidental_purchase là một giao dịch mua ngoài ý muốn, thường xảy ra trên thiết bị dùng chung.

6. Fraud là một giao dịch bị Google gắn cờ là gian lận.

7. Friendly_fraud là một chargeback trong đó chính chủ thẻ thực sự khiếu nại một giao dịch mà họ đã thực hiện.

8. Chargeback là việc ngân hàng của khách hàng đảo ngược khoản thanh toán, mang tính chung thẩm với ngân hàng và giờ đây được tính phí lại cho bạn.

9. Unacknowledged_purchase là việc Google tự động hoàn tiền cho một giao dịch mua mà ứng dụng của bạn chưa bao giờ acknowledge.

Reason 9 là một khoản hoàn tiền do chính bạn gây ra. Bất kỳ giao dịch mua nào mà ứng dụng của bạn để sót không acknowledge đều sẽ được Google hoàn tiền lại, vì vậy một unacknowledged_purchase trong feed này là doanh thu bị mất vì một lệnh gọi bị bỏ sót, chứ không phải vì bất kỳ lựa chọn nào của người mua.

Cửa sổ 30 ngày âm thầm làm hao hụt danh sách

API này không thể truy xuất ngược xa hơn 30 ngày trước. startTime mặc định là thời điểm hiện tại trừ đi 30 ngày và từ chối được đặt sớm hơn, còn endTime mặc định là thời điểm hiện tại, vì vậy những gì bạn nhận được là một cửa sổ trượt một tháng chứ không phải một kho lưu trữ vĩnh viễn.

Hệ quả của điều này rất rõ ràng. Nếu một tác vụ polling bị hỏng và không ai để ý trong năm tuần, thì các void từ tuần đầu tiên đã bị loại khỏi API, không có lệnh gọi nào có thể lấy lại chúng nữa. Những đơn hàng đó không được revoke, và bạn thậm chí sẽ không biết chúng từng xảy ra trừ khi bạn đã ghi nhận chúng ở nơi khác. Tấm lưới này có một lỗ hổng rộng đúng bằng khoảng thời gian gián đoạn dài nhất của bạn. Hãy polling ít nhất mỗi ngày một lần và đối chiếu với các thông báo thời gian thực, xem 30 ngày như chiếc đồng hồ xóa dữ liệu cứng mà nó thực sự là.

Việc một đơn hàng có xuất hiện hay không phụ thuộc vào revoke

Đây là lý do số một khiến các đội ngũ cho rằng API này bị hỏng. Nó chỉ trả về các đơn hàng đã revoke và không gì khác. Hoàn tiền do người dùng khởi tạo, hủy, chargeback, và hoàn tiền do Google khởi xướng đều tự động revoke, vì vậy chúng luôn xuất hiện trong feed. Hoàn tiền do nhà phát triển khởi tạo là ngoại lệ. Việc tự bạn thực hiện hoàn tiền, dù từ Play Console hay qua Orders API, để lại lựa chọn revoke như một quyết định riêng bạn phải tự đưa ra. Nếu bạn bỏ qua nó, đơn hàng vẫn được xử lý xong với khách hàng nhưng sẽ không bao giờ xuất hiện trong feed này.

Kết luận ngắn gọn là: khi mục tiêu của bạn là cắt quyền truy cập, hãy hoàn tiền với revoke được bật. Bỏ qua nó, bạn đã trả lại tiền trong khi vẫn để cửa mở, và tác vụ revoke của bạn, dù được xây dựng cẩn thận đến đâu, cũng chẳng có gì để xử lý.

Polling mà không chạm vào quota

Endpoint này bị giới hạn tốc độ (rate limited), và các mức trần đủ thấp để một vòng lặp bất cẩn có thể chạm vào chúng. Bạn được cấp 6,000 truy vấn mỗi ngày, tính theo giờ Thái Bình Dương (Pacific Time), và không bao giờ quá 30 truy vấn trong bất kỳ khoảng 30 giây nào. Ngân sách này phù hợp với polling theo khung thời gian và sẽ trừng phạt bất kỳ thiết kế nào gửi một lệnh gọi cho mỗi đơn hàng.

Khung thời gian và continuation token

maxResults mặc định là 1,000, và đó cũng là giá trị cao nhất có thể đạt được. Nếu một khung thời gian chứa nhiều void hơn số lượng một trang có thể chứa, response sẽ đi kèm một tokenPagination object chứa một nextPageToken. Hãy gửi lại token đó trong lệnh gọi tiếp theo để lật qua các trang. Cố định các mốc của khung thời gian bằng startTime và endTime, tiếp tục phân trang cho đến khi token cạn, và chỉ khi đó mới dịch chuyển khung thời gian đi tiếp. Nhịp độ này tránh được cả mức trần burst 30 giây lẫn hạn ngạch hàng ngày.

Real-time notifications lấp đầy khoảng trống hàng ngày

Ngay cả một lần polling mỗi ngày cũng khiến bạn "mù" tới 24 giờ, và những khoảng trống dài chính là điều mà cửa sổ 30 ngày trừng phạt. Real-time developer notifications sẽ lấp đầy độ trễ đó. Ngay khi một giao dịch mua bị voided, một VoidedPurchaseNotification sẽ được publish tới một Cloud Pub/Sub topic do bạn quản lý, và backend của bạn sẽ tiêu thụ nó trong vài giây. Payload của nó rất gọn nhẹ:

  • purchaseToken là token từ giao dịch mua ban đầu.

  • orderId là id của giao dịch bị voided, được tạo mới cho mỗi lần gia hạn subscription.

  • productType là 1 cho subscription và 2 cho giao dịch mua một lần.

  • refundType phân biệt một khoản hoàn tiền toàn phần, đánh dấu là 1, với một khoản hoàn tiền một phần dựa trên số lượng, đánh dấu là 2.

Hãy coi thông báo này như một cảnh báo chứ không phải chân lý tuyệt đối. Ngay khi một VoidedPurchaseNotification đến, hãy truy vấn Voided Purchases API để xác minh đơn hàng thực sự đang ở trạng thái nào, và chỉ revoke sau đó. Cảnh báo cho bạn biết cần kiểm tra; API cho bạn biết sự thật.

Cái giá phải trả bằng tiền

API này giống như hệ thống ống nước, nhưng lý do để lắp đặt đường ống chính là hóa đơn phải trả, và hai con số trên hóa đơn đó đang tăng lên.

Từ ngày 3 tháng 8 năm 2026, hóa đơn chargeback thuộc về bạn

Kể từ ngày 3 tháng 8 năm 2026, Google chuyển chi phí của một chargeback sang cho nhà phát triển. Bạn mất đi giá trị giao dịch mua và còn phải chi trả thêm phí chargeback của ngân hàng. Một voidedReason là 7 không còn đơn thuần là một giao dịch bán hàng bị mất nữa, mà trở thành một khoản mục kèm theo phí. Bản thân chargeback không thể bị đảo ngược, vì nó mang tính chung thẩm với ngân hàng, nhưng việc bắt kịp void nhanh chóng cho phép bạn revoke entitlement, và với bất cứ thứ gì vẫn đang được giao, dừng chi tiêu cho một khách hàng đã được hoàn tiền rồi sau đó bị đảo ngược.

Bạn vẫn tiếp tục chi trả cho một khách hàng đã được hoàn tiền

Giá trị giao dịch bán hàng mất đi ngay khi một void xuất hiện. Thứ bạn vẫn còn kiểm soát được là chi phí để tiếp tục giao hàng. Với mỗi giờ mà một entitlement đã hoàn tiền vẫn còn hoạt động, các hóa đơn mà khách hàng đã ngừng chi trả vẫn tiếp tục đổ về phía bạn: chi phí compute, các lệnh gọi tới model provider, lưu trữ, và bất kỳ khoản trả cho creator hay partner nào mà hoạt động của họ kích hoạt. Xây dựng pipeline revoke dựa trên API này chính là cách để ngắt khoản chi đó. Bỏ qua nó và bạn đang tự bỏ tiền túi tài trợ sản phẩm cho những người mà cửa hàng đã hoàn tiền rồi.

Friendly fraud là một xu hướng, không phải một sự cố đơn lẻ

Một voidedReason là 5 hoặc 6 hiếm khi đứng một mình. Fraud và friendly fraud thường tập trung quanh các tài khoản, thiết bị, và đôi khi là các chương trình khuyến mãi cụ thể. Vì API gắn voidedSource và voidedReason vào mỗi void, bạn có đủ dữ liệu để theo dõi xu hướng lạm dụng theo từng tài khoản thay vì hấp thụ mỗi lần đảo ngược như một khoản lỗ riêng lẻ. Một tài khoản chargeback lần thứ hai đang cho bạn biết điều mà lần hoàn tiền đầu tiên chưa nói ra.

Ghép mọi thứ lại với nhau

Toàn bộ mô hình sẽ nhỏ gọn một khi bạn đã có đủ các mảnh ghép trong tay. Lắng nghe VoidedPurchaseNotification theo thời gian thực để không có gì phải chờ đợi suốt cả ngày. Coi Voided Purchases API là nguồn dữ liệu chân lý, dùng orderId làm khóa để các lần gia hạn không bao giờ lẫn lộn vào nhau. Đọc voidedSource và voidedReason để một chargeback được xử lý tách biệt với một khoản hoàn tiền do đổi ý. Polling theo một nhịp độ đủ chặt để cửa sổ 30 ngày không bao giờ kịp "cắn", và hoàn tiền với revoke được bật bất cứ khi nào bạn có ý định cắt quyền truy cập.

Đây chính là lớp mà Refund Sensor vận hành thay cho bạn. Nó tiêu thụ các thông báo thời gian thực, đối chiếu từng void với API, revoke đúng đơn hàng cụ thể thay vì toàn bộ sản phẩm, và tách biệt một chargeback ngân hàng khỏi một khoản hoàn tiền thông thường để những trường hợp tốn kém nổi lên thay vì ẩn đi. Bạn có được quyền truy cập bị cắt trong vài giây và một bản ghi đầy đủ về ai đã void cái gì và tại sao, mà không cần tự dựng bất kỳ Pub/Sub pipeline hay polling job nào.

Các quy định này được ghi ở đâu

Câu hỏi thường gặp

Vì feed này chỉ chứa các đơn đã bị thu hồi (revoked). Hoàn tiền do người dùng khởi tạo, cùng với các trường hợp hủy, chargeback và hoàn tiền do Google khởi xướng đều tự động revoke và luôn xuất hiện. Một khoản hoàn tiền do chính bạn thực hiện chỉ xuất hiện nếu bạn cũng chọn revoke nó. Hoàn tiền mà không revoke thì đơn hàng vẫn được xử lý xong nhưng lại vô hình ở đây, vì vậy hãy bật revoke bất cứ khi nào mục tiêu của bạn là thu hồi quyền truy cập.

Ba mươi ngày, không hơn. Theo mặc định, startTime là thời điểm hiện tại trừ đi 30 ngày và không chấp nhận giá trị sớm hơn, vì vậy endpoint này hoạt động như một cửa sổ trượt một tháng thay vì một kho lưu trữ. Khi một void đã quá 30 ngày, nó biến mất và không có cách nào lấy lại được, đó chính là lý do bạn cần polling theo lịch trình và bổ sung bằng các thông báo thời gian thực.

Cả hai đều có vai trò riêng. VoidedPurchaseNotification đến tay bạn trong vài giây và báo cho bạn biết cần kiểm tra, nhưng hướng dẫn của Google là hãy xem nó như một tín hiệu chứ không phải sự thật tuyệt đối. Hãy xác nhận trạng thái hiện tại qua Voided Purchases API, rồi mới revoke. Thông báo giúp loại bỏ độ trễ; còn API cung cấp voidedSource và voidedReason chính xác mà bạn thực sự dựa vào để hành động.

Hãy nhìn vào voidedReason. Số 7 là chargeback, tức ngân hàng của khách hàng đã đảo ngược giao dịch, còn số 6 là friendly fraud. Số 1 là một khoản hoàn tiền thông thường do đổi ý. Sự phân biệt này quan trọng vì kể từ ngày 3 tháng 8 năm 2026, Google sẽ chuyển giá trị chargeback và phí ngân hàng sang cho nhà phát triển, nên một trường hợp số 7 sẽ tốn kém hơn nhiều so với một khoản hoàn tiền thông thường.

Có. Đặt tham số type thành 1, bạn sẽ nhận được cả các giao dịch mua trong ứng dụng (in-app) bị voided lẫn các giao dịch subscription bị voided; giá trị mặc định là 0 chỉ trả về các sản phẩm in-app. Với subscription, hãy xác định chính xác kỳ bị voided bằng orderId, vì một purchaseToken duy nhất trải dài qua mọi lần gia hạn, trong khi mỗi giao dịch gia hạn lại có orderId riêng.

#voided purchases api#refunds#chargebacks#revoke access#rtdn#play billing
Refund SensorRefund Sensor TeamRefund defense for App Store and Google Play developers