Google Play 上的退款悄无声息地发生。扣款被撤销,客户继续使用应用,而你这边除非主动查看,否则什么都不会改变。查看正是 Voided Purchases API 存在的意义。它会返回被取消、退款或拒付的订单,让你能够撤销客户已经停止付费的内容的访问权限。接入一个定时任务,读取返回结果,然后撤销相应权限。整个机制就是这样。
几乎每个团队都会在一个问题上栽跟头,而这个问题出在策略层面,而非代码层面。这个 API 只会列出真正被撤销(revoked)的订单。如果你在 Play Console 中退款时没有勾选 revoke 选项,那笔订单就永远不会出现在这里——于是你的任务运行得干干净净,而一个已经拿到退款的客户却带着你卖出的一切扬长而去。接下来的内容将逐个字段拆解这个 API、它的限制边界,以及在未接入时资金会在哪里悄悄流失。
核心要点
Voided Purchases API 通过 purchases.voidedpurchases.list 方法调用,列出 Google Play 已取消、已退款或已拒付的订单,这让你能够构建一套系统,剥夺客户已不再持有的购买项目的访问权限。
它只显示已撤销(revoked)的订单。开发者发起退款时如果没有同时撤销,该订单在这里就不可见,因此要撤销访问权限,就必须在退款时打开 revoke 选项。
它的查询范围是滚动的 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 次,因此应将每次请求限定在一个时间窗口内,并使用 continuation token 分页获取,而不要为每个订单单独发起一次请求。
这个 API 会返回什么
这个接口解决的是一个单一的问题:这个应用最近有哪些订单被作废了。一次作废(void)涵盖三种会把钱退还给客户的结果,即取消、退款或拒付。它既覆盖一次性的应用内商品,也覆盖订阅,而作用范围由一个参数决定。将 type 保留为默认值 0,返回的只是已作废的应用内商品购买记录。将 type 设为 1,你会同时收到已作废的应用内购买和已作废的订阅购买。
响应中的每一项都是一个 voided purchase 对象,包含一组简短但信息量很高的字段:
orderId 唯一标识一次性购买、一个订阅,或订阅中的某一次具体续订。把它当作你的关联键(join key)。
purchaseToken 标识一次性购买或一个订阅,但无法区分各次续订,遇到续订时应依赖 orderId。
purchaseTimeMillis 记录购买发生的时间,以自纪元(epoch)以来的毫秒数表示。
voidedTimeMillis 记录该购买被取消、退款或拒付的时间,同样以自纪元以来的毫秒数表示。
voidedSource 标明是谁发起了作废,其中 0 代表用户,1 代表开发者,2 代表 Google。
voidedReason 以 0 到 8 之间的整数给出具体原因。
voidedQuantity 携带基于数量的部分退款(quantity-based partial refund)中被作废的数量,仅在 includeQuantityBasedPartialRefund 为 true 时才会出现。
由于一个 purchaseToken 会覆盖整个订阅,而每次续订都会生成一个全新的 orderId,如果你用这个 token 作为权益的关联键,就会撤销错误的计费周期。应该改用 orderId 作为关联键。
在采取任何行动之前,先读懂 voidedReason
voidedReason 正是把一份普通列表转化为实际决策依据的关键,因为“改变主意”式的退款和银行拒付虽然出现在同一个信息流中,但性质完全不同。完整的取值如下:
1. Other 没有被归入具体类别。直接撤销权限,继续处理即可。
2. Remorse 指客户改变了主意,是日常可见的退款。
3. Not_received 是客户声称从未收到商品,值得检查一下你的发货/交付环节。
4. Defective 表示商品无法使用,这是一个应当记录下来的质量信号。
5. Accidental_purchase 是非本意的购买,常见于共享设备。
6. Fraud 是被 Google 标记为欺诈的交易。
7. Friendly_fraud 是一种拒付,持卡人本人确实发起了这笔交易,却对其提出异议。
8. Chargeback 是客户的银行撤销了这笔付款,银行方面已成定局,费用现在将由你承担。
9. Unacknowledged_purchase 是 Google 对一笔你的应用从未确认(acknowledge)的购买自动发起的退款。
原因 9 是你自己招致的退款。任何你的应用没有确认的购买,Google 都会将其退款,因此这个信息流中出现的 unacknowledged_purchase,其实是因为遗漏了一次调用而拱手让出的收入,而非买家做出的任何选择。
悄悄抹掉列表内容的 30 天窗口
这个 API 最多只能回溯过去 30 天。startTime 默认是当前时间减去 30 天,且不允许设置得更早;endTime 默认是当前时间,所以你得到的是一个滚动的一个月窗口,而不是一份永久存档。
这意味着后果相当严重。假如一个轮询任务中断且五周内无人察觉,那么第一周的作废记录早已从 API 中消失,任何调用都无法再取回它们。这些订单的权限始终没有被撤销,而且除非你在别处已经捕获过,否则你根本不会知道发生过这件事。这张网上会出现一个和你最长一次中断同样宽的漏洞。至少应每天轮询一次,并与实时通知做对账,把这 30 天当作一个真正会硬性删除数据的计时器来对待。
一笔订单会不会出现,全看有没有 revoke
这是团队们抱怨这个 API “坏了”的头号原因。它返回的只是已撤销的订单,别无其他。用户发起的退款、取消、拒付,以及 Google 发起的退款,都会自动触发撤销,因此它们总会出现在这个信息流中。开发者发起的退款是个例外。无论你是通过 Play Console 还是通过 Orders API 自行发起退款,是否撤销权限都是一个需要你单独做出的决定。如果你没有选择撤销,那么这笔订单会与客户完成结算,却永远不会出现在这个信息流里。
结论很简单。当你的目标是切断访问权限时,退款时请同时打开 revoke 选项。如果跳过这一步,你就只是把钱退了回去,却仍然敞开着大门,而你的撤销任务无论构建得多么用心,都将无米下炊。
在不触碰配额上限的前提下进行轮询
这个接口有速率限制,而且上限低到一个粗心的循环就能触发它。你每天可以进行 6,000 次查询(按太平洋时间计算),且任意 30 秒内不得超过 30 次。这样的额度适合按窗口轮询,而会惩罚任何“每个订单一次调用”的设计。
时间窗口与 continuation token
maxResults 默认值是 1,000,这也是它能达到的上限。如果某个窗口内的作废记录数量超出一页能容纳的范围,响应中会附带一个 tokenPagination 对象,其中包含一个 nextPageToken。在下一次调用时传回这个 token,即可继续翻页。用 startTime 和 endTime 固定窗口的边界,持续翻页直到 token 耗尽,之后再移动窗口。这样的节奏既不会触碰 30 秒内的突发上限,也不会超出每日额度。
实时通知填补了每日的空档
即便每天轮询一次,也会让你出现长达 24 小时的盲区,而正是这种长时间的空档会被 30 天窗口所惩罚。实时开发者通知(real-time developer notifications)填补了这种延迟。购买一旦被作废,一条 VoidedPurchaseNotification 就会立即发布到你控制下的一个 Cloud Pub/Sub 主题,你的后端可以在几秒钟内消费它。它的负载内容十分精简:
purchaseToken 是原始购买的 token。
orderId 是被作废交易的 id,每次订阅续订都会生成一个全新的 id。
productType 为 1 代表订阅,为 2 代表一次性购买。
refundType 用于区分全额退款(标记为 1)和基于数量的部分退款(标记为 2)。
把这条通知当作一个提醒信号,而非绝对真理。一旦 VoidedPurchaseNotification 到达,就去查询 Voided Purchases API,核实这笔订单的实际状态,然后才撤销权限。这个提醒告诉你该去核查了;而这个 API 才会告诉你真相。
它会给你造成怎样的资金损失
这个 API 本质上只是管道,但铺设这条管道的原因是一张账单,而账单上有两个数字正在不断攀升。
从 2026 年 8 月 3 日起,拒付账单由你来承担
自 2026 年 8 月 3 日起,Google 会把拒付的成本转嫁给开发者。你不仅会失去购买金额,还要额外承担银行收取的拒付手续费。voidedReason 为 7 不再只是单纯的一笔销售损失,而是一笔附带手续费的账目。拒付本身无法被撤回,因为它在银行那里已成定局,但及时捕捉到这次作废,可以让你撤销相应权限,并且对于仍在交付中的内容,停止继续为一个已经退款又被拒付的客户花钱。
你仍在为一个已经获得退款的客户持续买单
一旦作废发生,销售金额就已经没了。你仍能掌控的是继续交付所产生的开支。一个已退款的权益每多保持一小时的有效状态,原本应由客户承担的费用就会继续落在你这边:算力、模型提供商的调用费用、存储费用,以及他们的活动所触发的任何创作者或合作伙伴分成。基于这个 API 搭建撤销权限的管道,正是关闭这笔支出的方式。跳过这一步,你就是在为那些应用商店已经退款的客户继续埋单。
Friendly fraud 是一种趋势,而非孤立事件
voidedReason 为 5 或 6 的情况很少是孤立出现的。欺诈和 friendly fraud 往往会聚集在特定账户、设备,有时还会聚集在特定的促销活动周围。由于这个 API 为每一次作废都附带了 voidedSource 和 voidedReason,你完全可以据此按账户追踪滥用趋势,而不是把每一次撤销都当作一笔孤立的损失来消化。一个第二次发起拒付的账户,正在告诉你第一次退款时没有告诉你的信息。
把这一切整合起来
一旦各个部分都备齐,整个模型其实很小。实时监听 VoidedPurchaseNotification,这样就不会有任何东西要等上整整一天。把 Voided Purchases API 当作唯一真相来源(source of truth),以 orderId 作为关联键,这样各次续订就不会混淆。读取 voidedSource 和 voidedReason,让拒付与“改变主意”式的退款区别对待。轮询的节奏要足够紧凑,以免被 30 天窗口咬上一口,并且在任何打算撤销访问权限的场合,都要在退款时启用 revoke。
这正是 Refund Sensor 替你运行的那一层。它负责消费实时通知,将每一次作废都与这个 API 进行核对,精确撤销对应的订单而不是整个产品的权限,并将银行拒付与普通退款区分开来,让代价高昂的那些问题浮出水面,而不是藏匿起来。你可以在几秒钟内撤销访问权限,并获得一份完整记录,说明是谁、在什么时候、因为什么原因作废了购买——而你完全不需要自己搭建 Pub/Sub 管道或轮询任务。
这些规则的官方文档出处
常见问题
因为这个信息流只携带已撤销(revoked)的订单。用户发起的退款,以及取消、拒付和 Google 发起的退款,都会自动触发撤销,因此始终会出现在其中。而你自己发起的退款,只有在你同时选择撤销的情况下才会出现。只退款而不撤销,订单就会完成结算,却在这里无从查到,因此只要你的目的是撤销访问权限,就要打开 revoke 选项。
最多三十天,不能再远了。startTime 默认是当前时间减去 30 天,并且不接受任何更早的值,因此这个接口的行为就像一个滚动的一个月窗口,而不是一份存档。一旦某次作废超过 30 天,就彻底消失,再也无法取回,这正是为什么你需要按计划定期轮询,并以实时通知作为后备手段。
两者各有各的作用。VoidedPurchaseNotification 会在几秒钟内到达,提醒你去核查,但 Google 的建议是把它当作一个信号,而不是真相本身。通过 Voided Purchases API 确认当前状态,然后再撤销权限。通知消除了延迟;而这个 API 提供了你真正据以行动的、权威的 voidedSource 和 voidedReason。
查看 voidedReason。7 代表拒付,即客户的银行撤销了这笔付款;6 代表 friendly fraud。1 则是普通的“改变主意”式退款。这个区分很重要,因为从 2026 年 8 月 3 日起,Google 会把拒付的金额和银行手续费都转嫁给开发者,所以 7 给你造成的损失会比普通退款更大。
包含。将 type 参数设为 1,你就会同时收到已作废的应用内购买和已作废的订阅购买;默认值 0 只返回应用内商品。对于订阅,应通过 orderId 来锁定具体被作废的计费周期,因为单个 purchaseToken 会贯穿每一次续订,而每一次续订交易都拥有各自的 orderId。



