当 Google Play 的一笔购买被退款或发生拒付时,Voided Purchases API 就是你得知此事的途径
当一笔购买被退款或拒付时,Google Play 会悄悄地将其作废。Voided Purchases API 就是这些订单的清单,好让你可以撤销访问权限。这里讲清每个字段、30 天的时间窗口、会隐藏订单的撤销选项,以及它的成本。

要点
- Voided Purchases API,即 purchases.voidedpurchases.list 方法,会返回 Google Play 已取消、已退款或已拒付的订单,好让你构建一套撤销系统,切断对客户不再拥有之物的访问。
- 只有被撤销的订单才会出现。开发者在不使用撤销选项的情况下发起的退款对这个 API 是不可见的,所以如果你想收回访问权限,就必须在开启撤销的情况下退款。
- 时间窗口是 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 次查询,所以要用延续令牌分页浏览结果,并按时间窗口查询,绝不要每个订单调用一次。
Google Play 上的退款不会来敲你的门。钱转走了,客户还开着这个应用,除非你主动去查,否则你这边什么都不会变。Voided Purchases API 就是你去查的地方。它给你一份被取消、退款或拒付的订单清单,好让你撤销对客户不再付费之物的访问。让一个定时任务指向它,读取清单,切断使用权。这就是整个循环。
有一个陷阱会让大多数团队栽跟头,而它不在代码里。只有被撤销的订单才会出现在这里。如果你在 Play Console 里退款一笔购买却没有勾选撤销选项,那笔订单永远不会到达这个 API,于是你的任务运行得干干净净,而一位已退款的客户却保留着你卖给他的一切。这篇文章逐个字段地讲解这个 API、限定它的数字,以及当你略过它时钱从哪里漏走。
Voided Purchases API 实际返回什么
这个 API 只回答一个问题:这个应用最近有哪些订单被作废了。作废涵盖三种结果,它们最终都以客户拿回自己的钱告终。取消、退款或拒付。它适用于一次性应用内商品和订阅,你用一个参数选择范围。把 type 设为 0,你就只得到已作废的应用内商品购买,这是默认值。设为 1,你就同时得到已作废的应用内购买和已作废的订阅购买。
清单中的每一条都是一个已作废购买对象。字段不多,而每一个都很重要。
一笔已作废购买上的字段
| 字段 | 它包含什么 |
|---|---|
| orderId | 唯一标识一次性购买、订阅购买或单次订阅续订的订单 id。这是你的连接键 |
| purchaseToken | 标识一次性购买或一项订阅的 token。它无法区分续订,所以对续订要用 orderId |
| purchaseTimeMillis | 购买发生的时间,以自纪元起的毫秒数表示 |
| voidedTimeMillis | 购买被取消、退款或拒付的时间,以自纪元起的毫秒数表示 |
| voidedSource | 谁发起了作废:0 用户,1 开发者,2 Google |
| voidedReason | 购买被作废的原因,一个 0 到 8 的整数 |
| voidedQuantity | 基于数量的部分退款所作废的数量,仅当 includeQuantityBasedPartialRefund 为 true 时返回 |
在行动之前先读 voidedReason
voidedReason 是把一份原始清单变成一个决定的字段。买家反悔的退款和银行拒付都落进同一份清单,但它们不是同一种事件,而八月的定价让其中之一变得昂贵。这里是完整的集合。
| voidedReason | 标签 | 它对你意味着什么 |
|---|---|---|
| 0 | Other | 未分配任何类别。撤销后继续 |
| 1 | Remorse | 买家改变了主意。一次普通的退款 |
| 2 | Not_received | 客户说他从未收到商品。值得检查你的交付 |
| 3 | Defective | 商品无法工作。一个质量信号,记录下来 |
| 4 | Accidental_purchase | 一次无意的购买,常见于共用设备 |
| 5 | Fraud | Google 将该交易标记为欺诈 |
| 6 | Friendly_fraud | 一次拒付,合法持卡人对自己发起的一笔扣款提出异议 |
| 7 | Chargeback | 客户的银行撤销了付款。银行终裁,且现在向你收费 |
| 8 | Unacknowledged_purchase | Google 自动退款了一笔你的应用从未确认的购买 |
30 天的时间窗口是掏空你清单的陷阱
Voided Purchases API 只能显示过去 30 天内的已作废购买。startTime 参数默认为当前时间减去 30 天,而且不能设得比那更早。endTime 默认为现在。所以这个 endpoint 是一个滚动的一个月窗口,而不是一个存档。
后果很直白。如果你的轮询任务坏了,而五周内没人注意到,那么第一周的作废已经在这个 API 里过期了。没有任何调用能把它们找回来。你不会撤销那些订单,你甚至不会知道它们存在过,除非你用其他某种方式捕获了它们。这个 API 是一张安全网,网上的洞和你最糟糕的一次停机一样大。
撤销选项决定一笔订单是否会出现
这是团队报告这个 API 坏了的最常见原因。只有被撤销的订单才会被返回。用户发起的退款、取消、拒付以及 Google 发起的退款始终会被撤销,所以它们始终会出现。开发者发起的退款则不同。当你自己通过 Play Console 或 Orders API 退款一笔订单时,你可以选择是否也撤销它。退款而不撤销,那笔订单就与客户结清了,却永远不会出现在 Voided Purchases API 中。
由此得出的规则很简单。如果你的意图是收回访问权限,就在开启撤销选项的情况下退款。否则你就是退还了钱却把门敞开,而你的撤销任务,无论写得多好,都没有可以处理的对象。
如何在不触发配额的情况下轮询它
这个 endpoint 有速率限制,而且限制低到一个天真的循环就会撞上它们。你每天有 6,000 次查询,按太平洋时间计算,且在任意 30 秒时段内不超过 30 次查询。这个预算对按窗口轮询来说没问题,对每个订单一次请求的设计则很不友好。
查询窗口与延续令牌
maxResults 默认为 1,000,这也是上限。当一个窗口里的作废超过一页时,响应会带一个含 nextPageToken 的 tokenPagination 对象。在下一次调用时把那个 token 传回来,以遍历各页。设置 startTime 和 endTime 来界定你关心的窗口,翻页直到令牌用尽,然后推进窗口。这种模式让你既待在 30 秒突发限制之内,也待在每日上限之内。
Real-time developer notifications 弥合了空隙
每天轮询仍会留下最多一天的盲区,而 30 天的窗口会惩罚长时间的间隔。Real-time developer notifications 消除了这种延迟。Google 会在一笔购买被作废的那一刻,把一条 VoidedPurchaseNotification 发布到一个你拥有的 Cloud Pub/Sub 主题,而你的后端会在几秒内消费它。这条消息很小。
| RTDN 字段 | 它包含什么 |
|---|---|
| purchaseToken | 来自原始购买的 token |
| orderId | 已作废交易的订单 id,每次订阅续订都是一个新的 |
| productType | 1 表示订阅,2 表示一次性购买 |
| refundType | 1 表示全额退款,2 表示基于数量的部分退款 |

这在金钱上会让你付出什么
这个 API 是管道,但把它接起来的理由是一张账单。那份清单里的每一次作废都对应一个真实的数字,而其中两个正变得更贵。
拒付账单从 2026 年 8 月 3 日起落到你头上
从 2026 年 8 月 3 日起,Google 把拒付的成本转移给开发者。你损失购买价款,还要额外支付银行的拒付费用。voidedReason 为 7 不再只是一笔损失的销售,它是一个附带费用的条目。你无法逆转拒付,它在银行那里是最终的,但你可以在此之后止血。快速捕捉到作废,能让你撤销使用权,并且对任何你仍在交付的东西,停止为一位被退款后又拒付的客户花钱。
你还在为服务一位已退款的客户持续付费
购买价款在一次作废出现的那一刻就没了。你仍能控制的是继续交付的成本。一个已退款的使用权每多存活一小时,你就还在为客户不再资助的东西付费:计算、模型 API 调用、存储,以及任何与他的使用挂钩的创作者或合作伙伴分成。一套由这个 API 驱动的撤销系统就是你关掉那块计价表的方式。略过它,你就是在为商店已经补偿过的人资助这个产品。
善意欺诈是一个值得追踪趋势的模式
voidedReason 为 5 或 6 不是一次性的。欺诈和善意欺诈会按账户、按设备、有时按促销活动聚集。这个 API 在每一次作废上都给你 voidedSource 和 voidedReason,这足以按账户追踪滥用的趋势,而不是把每一次撤销当作一项孤立的成本。一位拒付两次的客户在告诉你一些第一次退款没有说出的事。
以 RefundHalt 的方式把它接起来
一旦你掌握了所有部件,这个模型就很小。实时监听 VoidedPurchaseNotification,好让任何事都不必等上整整一天。调用 Voided Purchases API 作为事实的来源,以 orderId 为键,这样订阅续订永远不会被搞混。读取 voidedSource 和 voidedReason,好让拒付的处理不同于反悔退款。以足够紧凑的计划轮询,好让 30 天的窗口永远咬不到你,并且只要你的意图是切断访问,就在开启撤销选项的情况下退款。
这就是 RefundHalt 为你运行的部分。它消费实时通知,将每一次作废与 API 核对,撤销确切的那笔订单而不是整个商品,并把银行拒付与普通退款区分开来,好让昂贵的那些被标记出来,而不是被埋没。你能在几秒内撤销访问,并得到一份谁作废了什么、为什么作废的记录,而无需自己搭建一条 Pub/Sub 管道和一个轮询任务。
常见问题解答
- 为什么我已退款的订单没有出现在 Voided Purchases API 中?
- 因为只有被撤销的订单才会被返回。用户退款、取消、拒付以及 Google 发起的退款始终会被撤销并始终会出现。开发者发起的退款只有在你也选择了撤销选项时才会出现。如果你退款了一笔订单却没有撤销它,这笔订单就结清了,但对这个 API 是不可见的,所以只要你打算收回访问权限,就在开启撤销的情况下退款。
- Voided Purchases API 能追溯到多久以前?
- 三十天。startTime 参数默认为当前时间减去 30 天,且不能设得比那更早,所以这个 endpoint 是一个滚动的一个月窗口,而不是一个存档。一笔作废的订单一旦超过 30 天就从这个 API 中消失,无法取回,这就是为什么你要按计划轮询,并用实时通知作为后盾。
- 我应该用 Real-time developer notifications 还是 Voided Purchases API 来撤销访问权限?
- 两者都用。VoidedPurchaseNotification 会在几秒内到达并告诉你去看,但 Google 自己的指导是把它当作一个信号,而不是事实的来源。调用 Voided Purchases API 确认当前状态,然后撤销。通知消除了延迟,而 API 给你权威的 voidedSource 和 voidedReason 供你据以行动。
- 我如何在 API 中区分拒付和普通退款?
- 读取 voidedReason 字段。值为 7 是拒付,意味着客户的银行撤销了付款,而 6 是善意欺诈。值为 1 是反悔退款。这很重要,因为从 2026 年 8 月 3 日起,Google 会把拒付的购买价款和银行费用转嫁给开发者,所以 voidedReason 为 7 会比一笔普通退款让你付出更多。
- Voided Purchases API 涵盖订阅吗?
- 涵盖。把 type 参数设为 1,就能同时得到已作废的应用内购买和已作废的订阅购买。默认值 type 0 只返回应用内商品购买。对于订阅,通过 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 Store 退款通知,而 REFUND_REVERSED 会把这笔销售还给你
苹果通过 App Store Server Notifications V2 发送四种退款消息,而大多数应用只处理其中两种。REFUND 告诉你要撤销权益,REFUND_DECLINED 意味着保留这笔销售,REFUND_REVERSED 则把销售还给你,并要求你恢复此前撤销的内容。下面说明每一种各需要什么处理。
现在每一笔 Apple 退款请求都附带一个理由,而 consumptionRequestReason 就是你读取它的方式
自 WWDC24 起,每一个 Apple CONSUMPTION_REQUEST 都携带一个 consumptionRequestReason,也就是客户自己陈述的退款原因。它有五个取值,从 UNINTENDED_PURCHASE 到 LEGAL,每一个都应当改变你在 12 小时窗口内所回复的内容。下面讲清楚如何读懂每一个。