所有文章
Deep dive阅读需 8 分钟

当 Google Play 的一笔购买被退款或发生拒付时,Voided Purchases API 就是你得知此事的途径

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

一部智能手机旁放着一本纸质账簿、一把合上的黄铜挂锁,还有一枚正在滑走的硬币,展示了报告已退款和已拒付订单的 Google Play Voided Purchases API

要点

  • 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标签它对你意味着什么
0Other未分配任何类别。撤销后继续
1Remorse买家改变了主意。一次普通的退款
2Not_received客户说他从未收到商品。值得检查你的交付
3Defective商品无法工作。一个质量信号,记录下来
4Accidental_purchase一次无意的购买,常见于共用设备
5FraudGoogle 将该交易标记为欺诈
6Friendly_fraud一次拒付,合法持卡人对自己发起的一笔扣款提出异议
7Chargeback客户的银行撤销了付款。银行终裁,且现在向你收费
8Unacknowledged_purchaseGoogle 自动退款了一笔你的应用从未确认的购买

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,每次订阅续订都是一个新的
productType1 表示订阅,2 表示一次性购买
refundType1 表示全额退款,2 表示基于数量的部分退款
一只手在一部智能手机旁将一把黄铜挂锁扣在一叠收据上,展示了在一笔 Google Play 购买被作废后撤销访问权限

这在金钱上会让你付出什么

这个 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。

来源和延伸阅读

RefundHalt

App Store 和 Google Play 退款自动驾驶

继续阅读

下一项退款请求已经在路上。

读完又一封关于未能申辩退款的支持邮件所需的时间,足够您完成 RefundHalt 设置。