When a Google Play purchase is refunded or charged back, the Voided Purchases API is how you find out
Google Play voids a purchase quietly when it is refunded or charged back. The Voided Purchases API is the list of those orders, so you can revoke access. Here is every field, the 30-day window, the revoke option that hides orders, and what it costs.

Key takeaways
- The Voided Purchases API, the purchases.voidedpurchases.list method, returns orders that Google Play canceled, refunded, or charged back, so you can build a revocation system that cuts access to what the customer no longer owns.
- Only revoked orders appear. A developer refund issued without the revoke option is invisible to this API, so if you want to pull access, you have to refund with revoke turned on.
- The window is 30 days. startTime cannot be older than 30 days ago, so a server that stays down longer than a month loses those voided orders for good. Poll on a schedule.
- voidedSource tells you who voided the order: 0 is the user, 1 is the developer, 2 is Google. voidedReason tells you why, from 0 Other through 7 Chargeback and 8 Unacknowledged_purchase.
- Real-time developer notifications push a VoidedPurchaseNotification the moment a purchase is voided, but treat it as a signal. Call the Voided Purchases API for the authoritative list before you revoke.
- Identify subscription renewals by orderId, not purchaseToken. One purchaseToken covers every renewal of a subscription, so the token alone cannot tell two renewals apart.
- Quotas are 6,000 queries per day and 30 queries in any 30-second window, so page through results with the continuation token and query by time window, never one call per order.
A refund on Google Play does not knock on your door. The money moves, the customer keeps the app open, and unless you go looking, nothing on your side changes. The Voided Purchases API is where you go looking. It hands you a list of orders that were canceled, refunded, or charged back, so you can revoke access to what the customer no longer paid for. Point a scheduled job at it, read the list, cut the entitlement. That is the whole loop.
There is one catch that trips up most teams, and it is not in the code. Only orders that were revoked show up here. If you refund a purchase in the Play Console without checking the revoke option, that order never reaches this API, and your job runs clean while a refunded customer keeps everything you sold them. This post walks the API field by field, the numbers that bound it, and where the money leaks when you skip it.
What the Voided Purchases API actually returns
The API answers one question: which orders for this app were voided recently. A void covers three outcomes that all end with the customer getting their money back. A cancellation, a refund, or a chargeback. It applies to one-time in-app products and to subscriptions, and you choose the scope with a single parameter. Set type to 0 and you get voided in-app product purchases only, which is the default. Set it to 1 and you get voided in-app purchases and voided subscription purchases together.
Each entry in the list is a voided purchase object. The fields are few and every one of them matters.
The fields on a voided purchase
| Field | What it holds |
|---|---|
| orderId | The order id that uniquely identifies a one-time purchase, a subscription purchase, or a single subscription renewal. This is your join key |
| purchaseToken | The token that identifies a one-time purchase or a subscription. It does not distinguish renewals, so use orderId for those |
| purchaseTimeMillis | When the purchase was made, in milliseconds since the epoch |
| voidedTimeMillis | When the purchase was canceled, refunded, or charged back, in milliseconds since the epoch |
| voidedSource | Who initiated the void: 0 user, 1 developer, 2 Google |
| voidedReason | Why the purchase was voided, an integer from 0 to 8 |
| voidedQuantity | The voided quantity from a quantity-based partial refund, returned only when includeQuantityBasedPartialRefund is true |
Read voidedReason before you act
voidedReason is the field that turns a raw list into a decision. A buyer's remorse refund and a bank chargeback both land in the same list, but they are not the same event, and August pricing makes one of them expensive. Here is the full set.
| voidedReason | Label | What it means for you |
|---|---|---|
| 0 | Other | No category was assigned. Revoke and move on |
| 1 | Remorse | The buyer changed their mind. An ordinary refund |
| 2 | Not_received | The customer says they never got the product. Worth checking your delivery |
| 3 | Defective | The product did not work. A quality signal, log it |
| 4 | Accidental_purchase | An unintended buy, often a shared device |
| 5 | Fraud | Google flagged the transaction as fraudulent |
| 6 | Friendly_fraud | A chargeback where the legitimate cardholder disputes a charge they made |
| 7 | Chargeback | The customer's bank reversed the payment. Bank-final, and now billed to you |
| 8 | Unacknowledged_purchase | Google auto-refunded a purchase your app never acknowledged |
The 30-day window is the trap that empties your list
The Voided Purchases API can only show voided purchases from the past 30 days. The startTime parameter defaults to the current time minus 30 days, and it cannot be set older than that. endTime defaults to now. So the endpoint is a rolling one-month window, not an archive.
The consequence is blunt. If your polling job breaks and nobody notices for five weeks, the voids from week one have aged out of the API. There is no call that brings them back. You will not revoke those orders, and you will not even know they existed unless you captured them some other way. The API is a safety net with a hole sized to your worst outage.
The revoke option decides whether an order even appears
This is the single most common reason a team reports the API as broken. Only revoked orders are returned. User-initiated refunds, cancellations, chargebacks, and Google-initiated refunds are always revoked, so they always appear. A developer-initiated refund is different. When you refund an order yourself, through the Play Console or the Orders API, you choose whether to also revoke it. Refund without revoke, and the order is settled with the customer but never surfaces in the Voided Purchases API.
The rule that follows is simple. If your intent is to pull access, refund with the revoke option on. Otherwise you have handed back the money and left the door open, and your revocation job, however well written, has nothing to act on.
How to poll it without tripping the quota
The endpoint is rate limited, and the limits are low enough that a naive loop will hit them. You get 6,000 queries per day, counted in Pacific Time, and no more than 30 queries in any 30-second period. That budget is fine for windowed polling and hostile to one-request-per-order designs.
Query windows and the continuation token
maxResults defaults to 1,000, which is also the ceiling. When a window holds more than one page of voids, the response carries a tokenPagination object with a nextPageToken. Pass that token back on the next call to walk the pages. Set startTime and endTime to bound the window you care about, page until the token runs out, then advance the window. That pattern keeps you inside both the 30-second burst limit and the daily cap.
Real-time developer notifications close the gap
Polling every day still leaves up to a day of blindness, and the 30-day window punishes long gaps. Real-time developer notifications remove the lag. Google publishes a VoidedPurchaseNotification to a Cloud Pub/Sub topic you own the moment a purchase is voided, and your backend consumes it within seconds. The message is small.
| RTDN field | What it holds |
|---|---|
| purchaseToken | The token from the original purchase |
| orderId | The order id for the voided transaction, a new one per subscription renewal |
| productType | 1 for a subscription, 2 for a one-time purchase |
| refundType | 1 for a full refund, 2 for a quantity-based partial refund |

What this costs you in money
The API is plumbing, but the reason to wire it up is a bill. Every void in that list maps to a real number, and two of them are getting more expensive.
The chargeback bill lands on you from August 3, 2026
Starting August 3, 2026, Google shifts the cost of a chargeback onto the developer. You lose the purchase price and you pay the bank's chargeback fee on top. A voidedReason of 7 is no longer just a lost sale, it is a line item with a fee attached. You cannot reverse a chargeback, it is final with the bank, but you can stop the bleeding after it. Catching the void fast lets you revoke the entitlement and, for anything you are still delivering, stop spending on a customer who was refunded and then reversed.
You keep paying to serve a refunded customer
The purchase price is gone the moment a void appears. What you still control is the cost of continuing to deliver. Every hour a refunded entitlement stays live, you keep paying for the things the customer no longer funds: compute, model API calls, storage, and any creator or partner payout tied to their usage. A revocation system driven by this API is how you shut that meter off. Skip it, and you fund the product for people the store already made whole.
Friendly fraud is a pattern worth trending
A voidedReason of 5 or 6 is not a one-off. Fraud and friendly fraud cluster by account, by device, and sometimes by promotion. The API gives you the voidedSource and voidedReason on every void, which is enough to trend abuse by account rather than treating each reversal as an isolated cost. A customer who charges back twice is telling you something the first refund did not.
Wiring it up the RefundHalt way
The model is small once you hold all the pieces. Listen for VoidedPurchaseNotification in real time so nothing waits a full day. Call the Voided Purchases API as the source of truth, keyed on orderId so subscription renewals never get confused. Read voidedSource and voidedReason so a chargeback is handled differently from a remorse refund. Poll on a schedule tight enough that the 30-day window never bites, and refund with the revoke option on whenever your intent is to cut access.
This is the part RefundHalt runs for you. It consumes the real-time notifications, reconciles every void against the API, revokes the exact order rather than the whole product, and separates a bank chargeback from an ordinary refund so the expensive ones get flagged, not buried. You get access revoked in seconds and a record of who voided what and why, without standing up a Pub/Sub pipeline and a polling job yourself.
Frequently asked questions
- Why are my refunded orders not showing in the Voided Purchases API?
- Because only revoked orders are returned. User refunds, cancellations, chargebacks, and Google-initiated refunds are always revoked and always appear. A developer-initiated refund only appears if you also chose the revoke option. If you refunded an order without revoking it, the order is settled but invisible to this API, so refund with revoke on whenever you intend to pull access.
- How far back does the Voided Purchases API go?
- Thirty days. The startTime parameter defaults to the current time minus 30 days and cannot be set older than that, so the endpoint is a rolling one-month window rather than an archive. A voided order that ages past 30 days is gone from the API with no way to retrieve it, which is why you poll on a schedule and back it with real-time notifications.
- Should I use real-time developer notifications or the Voided Purchases API to revoke access?
- Use both. The VoidedPurchaseNotification arrives within seconds and tells you to look, but Google's own guidance is to treat it as a signal, not a source of truth. Call the Voided Purchases API to confirm the current state, then revoke. The notification removes the lag, and the API gives you the authoritative voidedSource and voidedReason to act on.
- How do I tell a chargeback from an ordinary refund in the API?
- Read the voidedReason field. A value of 7 is a chargeback, meaning the customer's bank reversed the payment, and 6 is friendly fraud. A value of 1 is a remorse refund. This matters because from August 3, 2026 Google passes the chargeback purchase price and the bank fee to the developer, so a voidedReason of 7 costs you more than a plain refund does.
- Does the Voided Purchases API cover subscriptions?
- Yes. Set the type parameter to 1 to get both voided in-app purchases and voided subscription purchases. The default, type 0, returns in-app product purchases only. For subscriptions, identify the exact voided period by orderId, because one purchaseToken covers every renewal and a new orderId is generated for each renewal transaction.
Sources and further reading
- 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
The refund autopilot for the App Store and Google Play
Keep reading
Three App Store refund notifications arrive after Apple decides, and REFUND_REVERSED gives the sale back
Apple sends four refund messages through App Store Server Notifications V2, and most apps handle only two. REFUND tells you to revoke, REFUND_DECLINED means keep the sale, and REFUND_REVERSED hands the sale back and asks you to restore what you took. Here is what each one requires.
Every Apple refund request now comes with a reason, and consumptionRequestReason is how you read it
Since WWDC24, every Apple CONSUMPTION_REQUEST carries a consumptionRequestReason, the customer's own stated reason for wanting a refund. There are five values, from UNINTENDED_PURCHASE to LEGAL, and each one should change what you send back inside your 12-hour window. Here is how to read every one.