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

Apple 可以撤销它已经批准的退款,而你的服务器若忽略这种已撤销的退款,就会把付了钱的客户挡在门外

当 App Store 撤销它已经批准的退款时,它期望你的服务器恢复你此前收回的访问权限。以下说明退款、退款被拒和退款撤销这些通知在 App Store 和 Google Play 上如何运作,以及当你忽略它们时每一种各会付出什么代价。

手机上一把发光的挂锁,旁边是一张银行账单和一把够不到的钥匙,代表一笔已撤销的退款把付了钱的客户挡在门外

要点

  • 当 App Store 因客户提出异议而撤销它此前批准的退款时,它会发送一条 REFUND_REVERSED 通知,而 Apple 的指示很明确:如果你的 app 收回了内容或服务,就需要把它们恢复。
  • 一条 REFUND 通知意味着 App Store 已经对该交易退款,因此你的服务器应当收回该访问权限。一条 REFUND_DECLINED 通知意味着 Apple 拒绝了该请求,客户同时保留其访问权限和该笔扣款。
  • 如果你在退款时收回权限却从不处理撤销,那么扣款被恢复的客户就会一直被挡在门外。那会带来一张支持工单、一条一星评价,而在 Google Play 上,这种挫败感可能演变成一笔如今要花你钱的拒付。
  • Apple 的 revocationReason 告诉你退款为何发生:value 1 表示 Apple 因你 app 内实际存在或被认为存在的问题而退款,value 0 表示其他原因,例如误购。
  • Google Play 没有撤销通知。当一笔购买被作废时,它发送一条 VoidedPurchaseNotification,并为拒付单独发送一条 PendingRefundReviewNotification,其余部分你用 Voided Purchases API 的拉取模型来对账。
  • Google Play 给你 24 小时通过调用 orders.reviewrefund 来回应一条 PendingRefundReviewNotification,而且它只记录你的第一次调用。从 2026 年 8 月 3 日起,一笔输掉的拒付会让开发者损失售价减去 Google 的服务费再加上银行的手续费。
  • 以幂等方式处理 App Store 的退款通知。重复投递是正常现象,因此把每一次收回和恢复都以交易 id 为键,并让重复的通知成为无操作。

退款并不总是盖棺定论。App Store 可以在客户提出异议后撤销它已经批准的退款,而当那笔已撤销的退款到达你的服务器时,它带着一条指示:把访问权限还回去。大多数团队接好了普通的 REFUND 通知,切断客户的访问,然后就到此为止。他们从不构建另一半。于是当撤销到来时,什么都没有运行,一个正在再次付款的客户却被挡在他所买之物的门外。以下说明 App Store 和 Google Play 上这整套退款通知如何运作,以及当你忽略它们时每一种各会付出什么代价。

App Store 发送三条退款通知,而非一条

大多数退款处理只为单一事件而构建:钱退回去了,切断客户的访问。App Store Server Notifications V2 数据流实际上承载着三种不同的退款结果,它们要求三件不同的事。其中两种改变客户能访问的内容。另一种撤销第一种。以下是完整的一套,用 Apple 自己的话来说。

通知含义你的服务器要做什么
CONSUMPTION_REQUEST客户申请退款,Apple 想要消费数据在 12 小时内发送消费 payload
REFUNDApp Store 对该交易退了款收回该交易的访问权限
REFUND_DECLINEDApp Store 拒绝了该退款请求无需操作;客户保留访问权限和扣款
REFUND_REVERSEDApp Store 撤销了它已批准的退款恢复你收回的内容或服务

REFUND,每个团队都会处理的那条

当 App Store 处理一笔退款时,它会向你配置的 URL 发送一条 REFUND 通知,Apple 的定义很明确:它'表示 App Store 已成功为一次消耗型 In-App Purchase、一次非消耗型 In-App Purchase、一项自动续期订阅或一项非续期订阅完成了交易退款。'你存下这笔已退款的交易,收回它所购买的一切,Apple 要求你在 app 内用情境化的提示告诉客户发生了什么变化。这是每个人最先接好的通知,往往也是唯一的一条。

REFUND_DECLINED,那条不需要你做任何事的通知

REFUND_DECLINED 的含义正如其字面:'App Store 拒绝了一次退款请求。'客户提出了申请,Apple 说不,交易维持不变。客户的访问权限没有任何变化,所以你的权限逻辑在这里什么都不做。这条通知的价值在于记账。它为一次你也许曾用 CONSUMPTION_REQUEST 回应过的退款请求画上句号,并确认客户仍然拥有他所付费的东西。把它当作一条记录,而非一次操作。

REFUND_REVERSED,那条让团队措手不及的通知

这是大多数退款管线从不处理的通知。Apple 的定义毫不含糊:REFUND_REVERSED '表示 App Store 因客户提出的异议而撤销了此前批准的一笔退款。如果你的 app 因相关退款而收回了内容或服务,就需要把它们恢复。'读两遍。Apple 给了客户退款,你收回了访问权限,随后 Apple 判定这笔退款不应成立并把它撤回。扣款再次生效。客户付了钱,而如果你的服务器只会收回,他就仍被挡在门外。已撤销的退款是唯一一种会把访问权限加回来的退款事件,也是几乎没人为之构建的那一种。

一笔已撤销的退款究竟让你付出多少代价

漏掉一次撤销并不是四舍五入的误差。沿着钱在两个方向上走一遍,因为任何一半弄错都有代价。

漏掉撤销,你就把一个正在付款的客户挡在门外。Apple 已经恢复了扣款,所以客户又一次掏了钱,而你的 app 却拒绝给他所买之物。眼前的代价是支持时间,以及一笔你现在也许要自己发放的善意退款,这一次没有商店佣金退回来缓冲。更慢的代价是评价和流失,而在 Google Play 上,正是这种被挡在门外的挫败感会演变成一笔拒付。

漏掉最初的退款,你就会继续服务一个分文未付的客户。与之相对的错误是根本从不收回。一个已退款的客户仍在生成图像、调用你的 API、占满你的存储,就着一笔已经撤销的销售不断累积真实成本。算力、第三方调用和存储都是你已经花掉的钱,而这些都不会随退款回来。

  • 支持成本:一个真人处理一张工单,只为一份被你自己的代码移除、又从未恢复的访问权限。
  • 善意退款:把钱重新退给一个被你错误挡在门外的客户,而这份手动的善意举动没有任何商店佣金退回。
  • 浪费的开销:算力、API 调用和存储,被一个你从未切断的已退款账户消耗掉。
  • 拒付风险:在 Google Play 上,一个觉得自己被重复扣款的客户可以提出异议,而如今一场输掉的异议会落到你头上。

已撤销的退款和普通退款是同一条 webhook 数据流,只是指向相反的方向。处理其一而略过其二,你就会在两端都付出代价。

一只手把一张纸质收据沿着桌面往回拉,代表一笔已撤销的退款撤销了一笔你的服务器已经据以行动的 App Store 退款

Apple 为何撤销退款,以及如何解读 revocationReason

撤销并非随机。Apple 把它与'客户提出的异议'联系起来,也就是客户在事后对退款决定提出质疑。当退款最初被批准时,该交易带有一个 revocationDate 和一个 revocationReason,而在下游任何环节据以行动之前,那个原因值得一读。

  • revocationReason 1:App Store 退款是'因为你 app 内实际存在或被认为存在的问题。'那是关于你产品的一个信号,而不仅仅是关于这个客户。
  • revocationReason 0:App Store 退款是'出于其他原因,例如误购。'不附带任何关于 app 质量的信号。

当一条 REFUND_REVERSED 为那笔交易到来时,收回正在被撤销。你的恢复逻辑应当查出原始交易 id,确认你曾收回过它,并把访问权限原样放回去。

Google Play 不发送撤销通知,所以你改为对账

Google Play 的模型不同,如果你用同一个 webhook 处理器同时跑两家商店,这一差别就很重要。Google 没有 REFUND_REVERSED 的对应物。Google 的实时开发者通知把退款事件拆成两条消息,而撤销通过对账处理,而非推送。

作废购买通知

当一笔 Google Play 购买被作废时,你的服务器会收到一条 VoidedPurchaseNotification。它会指明 purchaseToken 和 orderId、一个为 subscription 或 one-time 的 productType,以及一个 refundType,后者要么是完整作废,要么是对多数量购买的按数量部分退款。Google 说这些数据足以找到正确的购买并调整访问权限。至于更多信息,它会把你引向 Voided Purchases API,一个拉取模型,列出你所查询的时间戳范围内的已作废订单。

拒付审查,及其 24 小时倒计时

拒付通过另一条消息到来,即 PendingRefundReviewNotification。当客户就一笔扣款向银行提出异议时,Google Play 发送这条通知并启动一个倒计时。你有 24 小时来调用 orders.reviewrefund,附上退款偏好和任何使用证据,以便 Google 能代表你对一笔不正当的拒付提出抗辩。Google 记录你的第一次调用,忽略其余的。这是 Google 与 Apple 的 CONSUMPTION_REQUEST 相对应的机制,是你在一场异议中发声的唯一窗口。

因为没有撤销推送,一笔 Google 抗辩并胜出的拒付不会以一个整洁的恢复事件到来。你要对照 Voided Purchases API 和你自己的记录来对账。教训与 App Store 上的相同:一笔已作废的订单并不总是永久的,而你的权限状态必须能够往回移动,而不只是往前。

退款事件App StoreGoogle Play
退款已批准REFUND 通知VoidedPurchaseNotification
退款被拒REFUND_DECLINED 通知无单独消息
退款被撤销REFUND_REVERSED 通知无推送;通过 Voided Purchases API 对账
异议证据窗口CONSUMPTION_REQUEST,12 小时PendingRefundReviewNotification,24 小时
谁可以发起退款仅 AppleGoogle,或你从 Orders 标签页发起

如何处理每一条退款通知而不把任何人挡在门外

你不需要为每家商店配备单独的管线。你需要一个能在两个方向上移动权限、并把每条消息都当作可能重复的处理器。

  • 构建恢复,而不只是收回。对于每一条在 REFUND 时移除访问的路径,都写出在 REFUND_REVERSED 时将其恢复的逆向逻辑,以同一个交易 id 为键。
  • 让它幂等。两家商店都可能不止一次投递同一条通知,所以把每一次收回和恢复都以交易或订单 id 为键,并让重复成为无操作。
  • 行动前先读原因。用 revocationReason 把一笔因 app 质量而起的退款和一笔误购区分开,并把因 app 质量而起的那些转给负责产品质量的人。
  • 按时回应证据窗口。在一条 CONSUMPTION_REQUEST 之后的 12 小时内发送 Apple 的消费数据,并在一条 PendingRefundReviewNotification 之后的 24 小时内调用 orders.reviewrefund。
  • 存下每一个事件。保留 REFUND_DECLINED 和原始通知,以便日后到来的一次撤销能与它所撤销的那笔退款相匹配。

这一切都不会改变退款是否发生。它改变的是,一笔已撤销的退款另一端的客户,是否终会察觉你的服务器出了错。

常见问题解答

App Store 上的 REFUND_REVERSED 通知是什么?
它是 App Store 在告诉你的服务器,它撤销了一笔此前批准的退款,因为客户提出了异议。Apple 的指示很明确:如果你的 app 因那笔退款而收回了内容或服务,就需要把它们恢复。扣款再次生效,所以客户应当拿回他的访问权限。
当我收到一条 REFUND_DECLINED 通知时应该做什么?
对客户的访问权限不做任何事。REFUND_DECLINED 意味着 App Store 拒绝了退款请求,所以交易维持不变,客户保留他所付费的东西。把它当作一条为退款请求收尾的记录,那次请求往往是你用 CONSUMPTION_REQUEST 回应过的。
当一笔退款或拒付被撤销时,Google Play 会发送通知吗?
不会。Google Play 没有与 Apple 的 REFUND_REVERSED 相对应的东西。当一笔购买被作废时它发送一条 VoidedPurchaseNotification,为拒付发送一条 PendingRefundReviewNotification,但一笔 Google 胜出的被抗辩拒付不会被推送回你这里。你要用 Voided Purchases API 和你自己的记录来对账。
我有多长时间回应一笔 Google Play 拒付?
24 小时。当 Google Play 发送一条 PendingRefundReviewNotification 时,你有 24 小时来调用 orders.reviewrefund,附上退款偏好和使用证据。Google 只记录你的第一次调用。从 2026 年 8 月 3 日起,一笔输掉的拒付会让开发者损失售价减去 Google 的服务费再加上银行的手续费。
在一笔已退款的 App Store 交易上,revocationReason 告诉我什么?
它告诉你 Apple 为何退款。value 1 意味着 Apple 因你 app 内实际存在或被认为存在的问题而退款,那是一个产品信号。value 0 意味着其他原因,例如误购。读它能让你把指向某个 bug 的退款和例行的退款区分开。

来源和延伸阅读

RefundHalt

App Store 和 Google Play 退款自动驾驶

继续阅读

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

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