A refund on Google Play arrives silently. The charge reverses, the customer keeps using the app, and nothing on your end changes unless you deliberately check. Checking is exactly what the Voided Purchases API is for. It returns the orders that were cancelled, refunded, or charged back, so you can withdraw access to whatever the customer stopped paying for. Wire a scheduled job to it, read what comes back, and revoke the entitlement. That is the entire mechanism.
One thing catches almost every team, and it lives in policy rather than code. This API only lists orders that were actually revoked. Refund a purchase in the Play Console without ticking the revoke option and that order never appears here, so your job runs perfectly clean while a refunded customer walks off holding everything you sold. What follows covers the API field by field, the limits that bound it, and where the money quietly drains when it goes unwired.
Key takeaways
The Voided Purchases API, reached through the purchases.voidedpurchases.list method, lists orders that Google Play cancelled, refunded, or charged back, which is what lets you build a system that strips access to purchases the customer no longer holds.
Revoked orders are the only ones it shows. A developer refund issued without revoking stays invisible here, so pulling access means refunding with revoke switched on.
Its reach is a rolling 30 days. Since startTime cannot go back further than 30 days, any server offline longer than a month loses those voids permanently, which is why polling has to run on a schedule.
voidedSource identifies who triggered the void: 0 for the user, 1 for the developer, 2 for Google. voidedReason explains why, running from 0 for Other up through 7 for Chargeback and 8 for Unacknowledged_purchase.
Real-time developer notifications fire a VoidedPurchaseNotification the instant a purchase is voided, but treat that as a prompt. Confirm against the Voided Purchases API before revoking anything.
Tell subscription renewals apart by orderId, never by purchaseToken. A single purchaseToken spans every renewal of a subscription, so the token on its own cannot separate one period from the next.
The ceilings are 6,000 queries a day and 30 inside any 30-second stretch, so bound each request to a time window and page through it using the continuation token, never one request per order.
What the API hands back
The endpoint resolves a single question: which of this app's orders were voided lately. A void bundles three outcomes that all return the customer's money, namely a cancellation, a refund, or a chargeback. It reaches both one-time in-app products and subscriptions, and one parameter sets the scope. Leave type at 0, the default, and only voided in-app product purchases come back. Set type to 1 and you receive voided in-app purchases and voided subscription purchases together.
Every item in the response is a voided purchase object with a short, high-value set of fields:
orderId uniquely marks a one-off purchase, a subscription, or one individual renewal within it. Treat this as your join key.
purchaseToken identifies a one-time purchase or a subscription, but it does not separate renewals, so lean on orderId for those.
purchaseTimeMillis records when the purchase happened, in milliseconds since the epoch.
voidedTimeMillis records when it was cancelled, refunded, or charged back, again in milliseconds since the epoch.
voidedSource names who started the void, where 0 is the user, 1 is the developer, and 2 is Google.
voidedReason gives the cause as an integer from 0 to 8.
voidedQuantity carries the voided count from a quantity-based partial refund, and only appears when includeQuantityBasedPartialRefund is true.
Because one purchaseToken covers an entire subscription while every renewal gets a freshly minted orderId, keying your entitlements on the token will have you revoking the wrong period. Key on orderId instead.
Read voidedReason before you touch anything
voidedReason is what converts a plain list into an actual decision, because a change-of-mind refund and a bank chargeback share the same feed yet are nothing alike. The full set reads:
1. Other carries no assigned category. Revoke it and carry on.
2. Remorse is a customer who changed their mind, an everyday refund.
3. Not_received is a claim the product never arrived, worth a look at your delivery.
4. Defective means it did not work, a quality signal you should log.
5. Accidental_purchase is an unintended buy, frequently on a shared device.
6. Fraud is a transaction Google flagged as fraudulent.
7. Friendly_fraud is a chargeback where the genuine cardholder disputes a charge they actually made.
8. Chargeback is the customer's bank reversing the payment, final with the bank and now billed to you.
9. Unacknowledged_purchase is Google auto-refunding a purchase your app never acknowledged.
Reason 9 is a refund you brought on yourself. Any purchase your app leaves unacknowledged gets its money returned by Google, so an unacknowledged_purchase in this feed is revenue surrendered to a single skipped call rather than to any choice the buyer made.
The 30-day window that quietly drains the list
The API reaches back no further than the past 30 days. startTime defaults to now minus 30 days and refuses to be set earlier, and endTime defaults to the present, so what you get is a rolling one-month window rather than a permanent archive.
The implication is stark. Let a polling job break and go unnoticed for five weeks, and the voids from the first week have already dropped out of the API, with no call able to retrieve them. Those orders go unrevoked, and you would not even know they happened unless you had captured them elsewhere. The net has a hole exactly as wide as your longest outage. Poll at least daily and reconcile against the real-time notifications, treating the 30 days as the hard deletion clock it actually is.
Whether an order shows up at all comes down to revoke
This is the number-one reason teams call the API broken. It returns revoked orders and nothing else. Refunds started by users, cancellations, chargebacks, and Google-initiated refunds all revoke automatically, so they always land in the feed. A developer-initiated refund is the exception. Issuing the refund yourself, whether from the Play Console or through the Orders API, leaves the revoke choice as a separate decision you have to make. Decline it and the order settles with the customer yet never shows up in this feed.
The takeaway is short. When your goal is to cut access, refund with revoke enabled. Skip it and you have returned the money while leaving the door open, and your revocation job, however carefully built, has nothing to work with.
Polling it without hitting the quota
The endpoint is rate limited, and the ceilings sit low enough that a careless loop trips them. You get 6,000 queries a day, counted in Pacific Time, and never more than 30 in any 30-second span. That budget suits windowed polling and punishes any one-call-per-order design.
Time windows and the continuation token
maxResults sits at 1,000 by default, and that is also as high as it goes. Should a window contain more voids than one page can hold, the response comes with a tokenPagination object holding a nextPageToken. Return that token on your next call to advance through the pages. Fix the window's edges with startTime and endTime, keep paging until the token runs dry, and only then shift the window along. That cadence stays clear of both the 30-second burst ceiling and the daily allowance.
Real-time notifications close the daily gap
Even a daily poll leaves you blind for up to 24 hours, and long gaps are precisely what the 30-day window punishes. Real-time developer notifications close that lag. The instant a purchase is voided, a VoidedPurchaseNotification is published to a Cloud Pub/Sub topic under your control, and your backend consumes it within seconds. Its payload stays compact:
purchaseToken is the token from the original purchase.
orderId is the id for the voided transaction, freshly minted per subscription renewal.
productType is 1 for a subscription and 2 for a one-time purchase.
refundType distinguishes a full refund, marked 1, from a quantity-based partial refund, marked 2.
Take the notification as an alert rather than gospel. Once a VoidedPurchaseNotification lands, query the Voided Purchases API to verify where the order actually stands, and revoke only then. The alert tells you to check; the API tells you the truth.
What it costs you in money
The API is plumbing, but the reason to lay the pipe is a bill, and two of the numbers on it are climbing.
From August 3, 2026, the chargeback bill is yours
As of August 3, 2026, Google moves the cost of a chargeback onto the developer. You forfeit the purchase price and cover the bank's chargeback fee on top of it. A voidedReason of 7 stops being merely a lost sale and becomes a line item with a fee attached. The chargeback itself cannot be reversed, since it is final with the bank, but catching the void quickly lets you revoke the entitlement and, for anything still being delivered, stop spending on a customer who was refunded and then reversed.
You keep funding a customer who was already refunded
The sale price is gone the moment a void lands. What you still govern is the expense of carrying on with delivery. For every hour a refunded entitlement stays active, the bills the customer stopped covering keep arriving on your side: the compute, the model provider calls, the storage, and whatever creator or partner payout their activity triggers. Building the revocation pipeline on this API is how that spend gets shut off. Skip it and you are underwriting the product for people the store already reimbursed.
Friendly fraud is a trend, not an incident
A voidedReason of 5 or 6 rarely stands alone. Fraud and friendly fraud cluster around accounts, devices, and occasionally particular promotions. Since the API attaches voidedSource and voidedReason to every void, you have enough to trend abuse per account rather than absorbing each reversal as a standalone loss. An account that charges back a second time is telling you what the first refund did not.
Putting it together
The whole model is small once the pieces are in hand. Listen for VoidedPurchaseNotification in real time so nothing waits out a full day. Treat the Voided Purchases API as the source of truth, keyed on orderId so renewals never blur together. Read voidedSource and voidedReason so a chargeback gets handled apart from a remorse refund. Poll on a cadence tight enough that the 30-day window never bites, and refund with revoke enabled any time you mean to cut access.
This is the layer Refund Sensor runs on your behalf. It consumes the real-time notifications, reconciles every void against the API, revokes the precise order rather than the whole product, and keeps a bank chargeback separate from an ordinary refund so the costly ones surface instead of hiding. You get access pulled within seconds and a full record of who voided what and why, with no Pub/Sub pipeline or polling job for you to stand up.
Where these rules are documented
Frequently asked questions
Because the feed carries revoked orders only. Refunds a user starts, along with cancellations, chargebacks, and Google-initiated refunds, all revoke on their own and always appear. A refund you issue yourself only shows up if you also chose to revoke it. Refund without revoking and the order is settled but invisible here, so switch revoke on whenever your aim is to withdraw access.
Thirty days, and no further. startTime sits at the current time minus 30 days by default and refuses any earlier value, so the endpoint behaves as a rolling one-month window instead of an archive. Once a void ages past 30 days it is gone with no way to pull it back, which is exactly why you poll on a schedule and back it with real-time notifications.
Both have a role. A VoidedPurchaseNotification reaches you within seconds and tells you to check, but Google's guidance is to treat it as a signal rather than the truth. Confirm the current state through the Voided Purchases API, then revoke. The notification kills the delay; the API supplies the authoritative voidedSource and voidedReason you actually act on.
Look at voidedReason. A 7 is a chargeback, where the customer's bank reversed the payment, and a 6 is friendly fraud. A 1 is an ordinary change-of-mind refund. The distinction matters because, from August 3, 2026, Google passes the chargeback price and the bank fee to the developer, so a 7 costs you more than a plain refund.
It does. Set the type parameter to 1 and you receive voided in-app purchases together with voided subscription purchases; the default of 0 returns in-app products alone. For subscriptions, pin down the exact voided period by orderId, since a single purchaseToken spans every renewal while each renewal transaction gets its own orderId.






