苹果做出裁定后会发出三种 App Store 退款通知,而 REFUND_REVERSED 会把这笔销售还给你
苹果通过 App Store Server Notifications V2 发送四种退款消息,而大多数应用只处理其中两种。REFUND 告诉你要撤销权益,REFUND_DECLINED 意味着保留这笔销售,REFUND_REVERSED 则把销售还给你,并要求你恢复此前撤销的内容。下面说明每一种各需要什么处理。

要点
- 苹果通过 App Store Server Notifications V2 发送四种与退款相关的消息。CONSUMPTION_REQUEST 索取你的证据,而 REFUND、REFUND_DECLINED 和 REFUND_REVERSED 会在苹果已经做出裁定后报告结果。
- REFUND 通知意味着 App Store 已对该笔交易退款。它携带 revocationDate 和 revocationReason,是你撤销与该单笔交易绑定权益的信号,而不是撤销该产品的每一次购买。
- revocationReason 有两个取值。1 表示因你的产品存在问题而批准退款,0 表示因其他原因批准退款。取值为 1 的情况是值得记录和跟踪趋势的质量信号。
- REFUND_DECLINED 表示苹果驳回了顾客的退款。你保留这笔销售,无需改动任何东西,但前提是你在裁定成为最终结果之前没有撤销过访问权限,这样才安全。
- REFUND_REVERSED 表示苹果撤销了它此前已批准的退款,通常发生在顾客对该退款提出异议之后。交易上的撤销字段会消失,苹果自身的指引是,如果你撤销过内容,就需要恢复它。
- 对全部四种通知都要以 HTTP 200 响应。如果你的服务器曾宕机而漏掉了某一条,Get Refund History 端点可以让你按交易 id 查询已退款的交易并进行对账。
- 针对过去某个订阅周期的退款并不总是意味着应当结束访问。如果一个更新的付费周期仍然有效,对旧交易执行撤销会切断一位仍在付费的顾客的服务。
苹果对你的退款做出裁定后,它并不会就此沉默。一旦结果确定,App Store 会向你的服务器发送三种 App Store 退款通知之一,每一种都要求不同的动作。REFUND 表示钱已经退掉,你应当收回访问权限。REFUND_DECLINED 表示顾客的请求失败,你保留这笔销售。REFUND_REVERSED 表示苹果撤销了它此前已经批准的退款,于是这笔销售重新归你所有,你需要归还此前收回的一切。大多数应用只接好了第一种,而悄悄忽略了另外两种。一位付费顾客就是这样被锁在了他们已经付过钱的东西之外。
这三种与 CONSUMPTION_REQUEST 不同,后者是唯一一种要求你回复的退款消息。裁定后的通知不想要争辩。它们要的是一个 HTTP 200 以及对顾客访问权限的正确改动。下面说明每一种的含义、承载事实的确切字段,以及处理不当时钱会从哪里漏掉。
四种退款通知,以及哪一种需要回复
App Store Server Notifications V2 是一条单一的信息流。你只把它指向一个 URL,苹果就会把每一种通知类型都发到那里,所以无论你是否处理,你其实都已经收到了全部四种退款消息。其中有四种类型涉及退款,而只有一种是提问。
| 通知 | 苹果在告诉你什么 | 你的动作 | 是否需要回复 |
|---|---|---|---|
| CONSUMPTION_REQUEST | 顾客申请了退款,苹果需要你的数据 | 在 12 小时内 Send Consumption Information | 需要,真实数据 |
| REFUND | App Store 已对该笔交易退款 | 撤销该笔交易的权益 | 不需要,HTTP 200 |
| REFUND_DECLINED | App Store 驳回了退款 | 保留访问,不改动任何东西 | 不需要,HTTP 200 |
| REFUND_REVERSED | 苹果撤销了它已批准的退款 | 恢复你此前撤销的内容 | 不需要,HTTP 200 |
REFUND 通知究竟在告诉你什么
当 App Store 成功地向顾客退回了一笔交易时,REFUND 就会触发。它适用于每一种购买类型:消耗型项目、非消耗型项目、自动续期订阅以及非续期订阅。通知内部经过签名的交易,现在携带了退款之前没有的两个字段,而这两个字段就是全部故事。
revocationDate 和 revocationReason 承载事实
revocationDate 是 App Store 对该笔交易退款或撤销它的 UNIX 时间,以毫秒为单位。revocationReason 告诉你退款的类别,它恰好有两个取值。
| revocationReason | 苹果的含义 | 你应从中读出什么 |
|---|---|---|
| 1 | 因产品存在问题而批准退款 | 一个质量或交付信号。记录它,跟踪趋势,并在某个产品或某个构建版本中寻找规律 |
| 0 | 因其他原因批准退款 | 一次普通退款。撤销权益,然后继续 |
一笔交易上出现 revocationDate 本身就是标志。如果你之后取回一笔交易,发现它带有 revocationDate,那么无论有没有通知,这次购买都已被退款。请同时读取它的原因,这样某个版本上出现的一波取值为 1 的退款潮就不会作为噪音从你眼前溜过。
按交易撤销,而不是按产品撤销
这里的陷阱是撤销得太多。一条 REFUND 只指名一笔交易。它并没有让你停用顾客对该产品 id 曾经进行过的每一次购买。苹果自身的指引是,在你切断任何东西之前,先检查顾客仍然持有哪些访问权限,因为权益之间会重叠。经典的情形是订阅:退款落在上个月的续期上,而这个月的续期正处于有效且已全额付款的状态。若你按产品撤销,你就为一笔针对已经结束的周期的退款,切断了一位当前仍在付费的顾客。
REFUND_DECLINED 意味着你已经赢了,所以不要把它撤回
当 App Store 驳回了顾客的退款请求时,REFUND_DECLINED 就会到来。顾客提出了申请,苹果说不行,你保留这笔销售。表面上没有什么要做的,而这正是关键。这条通知暴露的错误是另一个:过早撤销访问权限。
如果你的代码在苹果做出裁定之前就对 CONSUMPTION_REQUEST 做出反应、收回了顾客的访问权限,那么 REFUND_DECLINED 就是那个决定爆雷的时刻。苹果留下了你的钱,而你却把一位退款被驳回的顾客锁在了门外。这位顾客现在为一个他无法使用的产品付费,提交一张支持工单,并把这件事记在心里。修复办法是一条规则,而不是一个功能:在 REFUND 时撤销,绝不在请求时撤销。REFUND_DECLINED 只不过是苹果在确认,过早撤销本来就是错误的做法。
REFUND_REVERSED 是那条把钱还给你的通知
REFUND_REVERSED 是几乎没人处理的那一条,也是把钱还给你的那一条。当苹果撤销它此前已批准的退款时,它就会发送这条通知,通常发生在顾客对该退款提出异议之后。REFUND 此前给交易添加的撤销字段又被移除了,于是该笔购买再次显示为已付款。苹果用一句话说明了开发者的职责:如果你的应用因相关退款而撤销了内容或服务,就需要恢复它们。它适用于任何购买类型,从消耗型项目到自动续期订阅。
数周之后才到来的问题
开发者在苹果自己的论坛上提出的真正问题是时机。一条 REFUND_REVERSED 可能在最初的 REFUND 之后数周才到来,那时距订阅周期到期早已过去很久。那时你还要不要恢复访问?恢复该笔交易实际授予的东西,范围限定在该笔交易所覆盖的内容之内。对于消耗型或非消耗型项目,把解锁重新打开。对于已经过去的订阅周期,你并不是在发放新的时长,而是在更正记录,让顾客的历史准确无误,并让任何仍然有效的权益重新生效。恢复那一笔具体的交易,然后由你的重叠逻辑来判定当前哪些是有效的。

把这件事做对的价值在哪里
这些通知中的每一条都对应一个真实的数字,而处理不当的代价并不只是销售价格本身。
REFUND:别再为一位已退款的顾客付出服务成本
REFUND 一到,销售价格就已经没了。你仍然能控制的,是继续交付所带来的成本。一笔已退款的权益每多存活一小时,你就在为顾客不再付费的东西继续花钱:算力、模型 API 调用、存储,以及与其使用相关的任何创作者或合作方分成。在 REFUND 时及时撤销可以让这个计费表停下来。忽略这条通知就意味着你在为一个商店已经补偿过的人资助产品。
REFUND_DECLINED:别把一场胜利变成一次善意退款
当你过早撤销、而退款之后又被驳回时,你在账面上保住了这笔销售,却在实际中失去了它。付了钱的顾客无法使用产品,于是你接手了一场支持对话,并且往往还要给一笔酌情退款来把事情摆平。这是在为一笔从未有过风险的销售付两次钱。正确处理 REFUND_DECLINED 不需要任何成本,这正是为什么在 REFUND 之前保持访问权限不动,是你能采纳的最省钱的规则。
REFUND_REVERSED:最糟的组合是钱和访问权限都没了
忽略 REFUND_REVERSED,你就会走向全盘最糟的结局。你已经收了钱,而顾客却一无所有。他们已经联系过一次银行来撤销退款,而一个被锁在如今仍要为其付费的产品之外的人,很可能会第二次联系银行。下一次争议可能演变为银行卡拒付,那是由银行终裁的,代价比这笔销售本身高得多。REFUND_REVERSED 一到就恢复访问权限,是整个退款流程中最便宜的保险。
需要接好哪些环节
一旦模型对了,处理起来其实很小。把权益以交易 id 为键,让每一条通知都指向唯一一笔购买。收到 CONSUMPTION_REQUEST 时,在 12 小时内发送你的数据。收到 REFUND 时,撤销那笔交易。收到 REFUND_DECLINED 时,什么都不做。收到 REFUND_REVERSED 时,恢复。对所有通知都迅速返回 HTTP 200,然后在你自己的时间里做访问权限的改动。
对于通知留下的缺口,使用 Get Refund History 端点。如果你的服务器在一次故障中宕机、漏掉了一条 REFUND,就针对某个交易 id 调用 App Store Server API 的退款查询,路径为 /inApps/v2/refund/lookup/{transactionId},并读回带有 revocationDate 和 revocationReason 的已签名交易。它一次对一笔交易进行对账,并翻页浏览顾客的已退款购买,这样一个漏掉的 webhook 就不会变成一个被永久设错的权益。
这正是 RefundHalt 为你运行的部分。它监听全部四种类型,在 REFUND 时撤销,在 REFUND_DECLINED 时保持访问不动,并在 REFUND_REVERSED 时自动恢复,每一步都以确切的交易为键。一笔被撤销的退款不会在队列里干等而让付费顾客一直被锁在外面,而一笔被驳回的退款也绝不会触发一次你还得回退的撤销。
常见问题解答
- REFUND 和 REFUND_REVERSED 有什么区别?
- REFUND 表示 App Store 对一笔交易进行了退款,你应当撤销该权益;而 REFUND_REVERSED 表示苹果撤销了它已批准的退款,你应当恢复此前撤销的内容。两者成对出现:一笔购买可以先走 REFUND,之后如果顾客的异议被推翻,再走 REFUND_REVERSED。把你的访问权限改动以交易 id 为键,这样每条通知都作用于正确的那笔购买。
- 收到 REFUND 通知需要回传什么吗?
- 不需要。你对 REFUND、REFUND_DECLINED 和 REFUND_REVERSED 都以一个不带正文的 HTTP 200 响应。只有 CONSUMPTION_REQUEST 要求你发送数据,且需在 12 小时内通过 Send Consumption Information 端点发送。另外三种是苹果在报告一个裁定,而不是在提问。
- 收到 REFUND_DECLINED 通知时我该做什么?
- 什么都不改,因为顾客的退款被驳回了,你保留这笔销售。REFUND_DECLINED 唯一会给你带来工作量的情形,是你在苹果裁定之前就过早撤销了访问权限。在 REFUND 时撤销,而不是在 CONSUMPTION_REQUEST 时撤销,那么 REFUND_DECLINED 就成了一个确认,说明访问权限被正确地保持了原状。
- 当 REFUND_REVERSED 在退款数周后才到来时,我该恢复访问权限吗?
- 是的,恢复该笔具体交易所授予的权益。苹果说明,如果你的应用因相关退款而撤销了内容,就需要恢复它。对于消耗型或非消耗型项目,把解锁重新打开。对于已经过期的订阅周期,你是在更正记录,而不是发放新的时长,所以由你的重叠逻辑来判定当前哪些是有效的。
- 我该如何补上服务器漏掉的退款通知?
- 使用 App Store Server API 的 Get Refund History 端点,它在 /inApps/v2/refund/lookup/{transactionId} 按交易 id 查询某位顾客的已退款交易。它返回带有 revocationDate 和 revocationReason 的已签名交易,这样在一次故障之后,你无需等待一条已经触发过的通知就能对访问权限进行对账。它每次调用处理一个交易 id,并翻页浏览该顾客的已退款购买。
来源和延伸阅读
RefundHalt
App Store 和 Google Play 退款自动驾驶
继续阅读
现在每一笔 Apple 退款请求都附带一个理由,而 consumptionRequestReason 就是你读取它的方式
自 WWDC24 起,每一个 Apple CONSUMPTION_REQUEST 都携带一个 consumptionRequestReason,也就是客户自己陈述的退款原因。它有五个取值,从 UNINTENDED_PURCHASE 到 LEGAL,每一个都应当改变你在 12 小时窗口内所回复的内容。下面讲清楚如何读懂每一个。
Google Play 的拒付审查给你 24 小时反击,下面是你该发送的内容
当银行撤销一笔 Google Play 扣款时,Google 会向你的服务器发送一条 PendingRefundReviewNotification,并启动一个 24 小时的倒计时。通过 ReviewRefund API 用退款倾向和真实的消费证据来回应它,否则这场争议就会在没有你的情况下被裁定。下面是整个流程,逐个字段来看。