In-App Purchases & Subscriptions

如何自动回应 Apple 应用内购买的退款请求?

了解 Apple 退款自动化如何简化退款请求处理,减少应用开发者的人工操作。

5 min read
如何自动回应 Apple 应用内购买的退款请求?

我因为 Undelivered Consumption Percentage Non Zero Error 这个问题几乎耗费了一整天。倒不是因为它是个多复杂的错误,而是因为我把 delivery Status 设成了非 DELIVERED 的值,却把 consumption Percentage 留在了 60000,而 Apple 对这种组合的拒绝毫不含糊。

这算是绕了一圈才回到今天真正要讨论的问题。

如果你正在研究如何自动回应 Apple 的退款请求,这件事其实比大多数团队想象的要简单。真正困难的从来不是 API 调用本身,而是要有数据可以填进去。逐字段的详细讲解见 Apple CONSUMPTION_REQUEST 详解。这篇文章要讲的是如何把整个流程接通,让任何人都不需要半夜守着。

关键要点

  • Apple 退款自动化其实就是一个 webhook 加一次 PUT 请求。Apple 发送 CONSUMPTION_REQUEST,你需要在 12 小时内作出回应。

  • 你需要的是 In-App Purchase key,而不是 App Store Connect API key。两者不能互换。

  • 五个字段中有三个是必填的,其中两个字段的限制条件只会以报错的形式暴露出来。

  • 没有获得客户同意,你就根本不该回应,而且事后也无法补充获取同意。

  • 该接口已迁移到 v2。生产环境地址是 api.storekit.apple.com,而沙盒环境给你的时间是 5 分钟,而不是 12 小时。

Apple 退款请求对开发者来说究竟是如何运作的

客户打开 reportaproblem.apple.com,在最近 90 天的购买记录中找到对应的购买项目,选择一个理由,然后提交申请。如果你已经配置了 App Store Server Notifications V2,你的接口就会收到一个以已签名 JWS 负载形式发送的 CONSUMPTION_REQUEST,其中带有交易信息、商品信息,以及客户所选的理由。

这个 webhook 就是触发 App Store 应用内购买退款流程的全部环节。你的 dashboard 上不会显示任何东西,也没有队列可以查看。这正是为什么很多团队在因此损失了好几周的钱之后,才发现这套机制的存在。

然后你就只有 12 小时的时间。

有一点值得纠正一下,因为这个误解到处都在流传:这并不只适用于消耗型商品。Apple 目前的文档说明,CONSUMPTION_REQUEST 会针对任何商品类型发出:消耗型(consumable)、非消耗型(non-consumable)、非自动续期订阅(non-renewing subscription),以及自动续期订阅(auto-renewable subscription)。一些旧的指南仍然说只针对消耗型商品和订阅,那些说法已经过时了。

具体来说,“回应”意味着什么

你需要向一个接口 PUT 一个 JSON 请求体。

PUT https://api.storekit.apple.com/inApps/v2/transactions/consumption/{transactionId}

沙盒环境地址是 api.storekit-sandbox.apple.com,路径相同。成功时返回的是 202 Accepted,而不是 200,这一点常常让那些在断言状态码时没留意到的人栽跟头。

身份验证使用的是一个短期有效的 ES256 JWT,由 In-App Purchase key 签名生成,这个密钥在 App Store Connect 的 Users and Access 下的 Integrations 里生成。它不是你的 App Store Connect API key。.p8 文件只能在创建时下载一次,如果你把它弄丢了,就得重新生成一个新的,并轮换所有用到它的地方。不用问我是怎么知道的。

五个字段,以及其中两个会咬你一口的字段

请求体是一个 Consumption Request。其中三个字段是必填的,两个是可选的。

  • Customer Consented(必填)。只有在你确实获得了客户同意的情况下才能设为 true。后面会详细说明,因为这个字段决定了整件事能不能进行。

  • Delivery Status(必填)。说明购买的商品是否已送达并正常可用。

  • Sample Content Provided(必填)。说明购买前是否提供了免费样品、试用,或功能说明信息。

  • Consumption Percentage(可选)。表示客户消耗了多少,以千分之一为单位。取值范围为 0 到 100000,因此 45.2% 对应的数值是 45200。

  • Refund Preference(可选)。取值为 GRANT_FULL、GRANT_PRORATED、DECLINE 或 NO_PREFERENCE。

有两个限制条件,每一个都足够让你耗费一个下午。

如果 Delivery Status 不是 DELIVERED,那么 Consumption Percentage 就必须是 0。其他任何取值都会导致 Undelivered Consumption Percentage Non Zero Error 报错。这就是我那一天的遭遇。

而如果你对一个自动续期订阅发送 GRANT_PRORATED,就完全不要包含 consumptionPercentage 字段。Apple 会根据计费周期内已经经过的时间自行计算这个值,如果你自己填了,就会收到 Consumption Percentage Auto Renewable Subscription Error 的拒绝。至少这些错误名称还算说得清楚,这比我原本预期的要好。

同意是一道门槛,而不是走个形式

Apple 的文档在这一点上说得异常直接:如果客户没有同意,就不要回应这条通知。不是把 Customer Consented 设为 false 来回应,而是根本不要回应。

你也不能等退款请求到达之后再去补充获取同意,同意必须事先存在。这意味着相关条款必须写进你的服务条款或隐私政策,而且是在你上线这整套流程之前就写好,而不是等到第一次退款纠纷出现之后。Apple 的指引是,同意应当是自愿给出的、具体明确的、知情的、且不含糊的,选择加入(opt-in)优于选择退出(opt-out),获取同意完全是你自己的责任。

具体操作上:写一句话说明你可能会与 Apple 共享购买和使用数据以协助审核退款请求,并提供一种撤回同意的方式。记得找自己的法律顾问审核一下。我不是律师,这也不构成法律建议。

开发者如何让 App Store 退款请求自动化

按照我会采用的顺序,整条流程大致如下:

  1. 在 App Store Connect 的 App Information 下配置 Production Server URL,并选择 Version 2。Consumption request 不会通过 V1 传输。

  2. 在信任任何一个字段之前,先对照 Apple 的根证书验证 JWS 签名链。

  3. 把交易匹配到对应用户。如果你在购买时附加了 appAccountToken,这是最快的路径。如果没有,你就得按商品和日期反向查找,这很快就会变得混乱。

  4. 检查是否已获得同意。没有同意,就不要回应。记录下来,然后继续处理下一个。

  5. 获取真实的使用数据,不是估算值,也不是默认值。如果你回答不了“客户用了多少”这个问题,那才是你自动化流程中真正的缺口。

  6. 构建 ConsumptionRequest,并套用上面提到的两个限制条件。

  7. 把它 PUT 出去,确认返回 202,并保存你发送的负载内容。

第 5 步是大多数尝试卡住的地方。API 调用只需要一个下午就能搞定。而要为七周前的一笔购买提供真实、按用户维度统计的消耗数据,这是一个你也许还没解决的数据建模问题。

要怎样才能在生产环境中稳定运行

除了理想流程之外,还有四件事要考虑:

幂等性(Idempotency)。Apple 会重试通知,你的处理程序会看到同一个通知出现两次。要按通知的 UUID 做去重,否则你就会提交重复的回应。

要有真正能通知到人的告警机制。一个默默失败的处理程序比根本没有处理程序更糟,因为你会误以为自己已经做好了防护。

一套沙盒测试工具。沙盒环境给你的时间是 5 分钟而不是 12 小时,所以你没法随意手动测试,要用脚本来做。

此外还要为每个案例记录你发送了什么内容。当一笔退款被拒绝时,你会想知道自己提交的是哪个 preference,以及背后的原因。当某笔退款在你提交了 DECLINE 的情况下仍被批准时,你也会想知道这一点。

老实估算一下:如果你以前搭建过 webhook 相关的基础设施,把各个环节连接起来大概需要几天时间,而要真正信得过这套系统则需要几周。而这其中大部分工作量都不在 Apple 这一侧。

或者交给别的工具来做

如果你已经在用 RevenueCat 或 Adapty,可以先看看它们自带的功能。两者都已经加入了退款回应相关的功能,而且你的 receipt 和客户映射数据本来就已经在那里了。

专门针对这个场景打造的 Apple 退款管理软件能做得更彻底。 Refund Sensor 直接基于你已有的 App Store Connect key 运行,能在时限内为每一条请求配上交易证据作出回应,并为每个案例记录结果。无需 SDK,无需改动代码,大约五分钟即可接入完成。如果你想先比较一下其他选择,我们在 Apple 退款管理工具一文中对整个领域做了横向对比。

客户那一侧的流程,既然有人问起

关于如何为 App Store 购买项目申请退款,从用户的角度来说:打开 reportaproblem.apple.com,登录账号,在最近 90 天的购买记录中找到对应项目,选择一个理由。Apple 会告知客户,他们将在 48 小时内收到回复。

你也可以通过 StoreKit 的 refund request sheet,在你的应用内呈现同样的表单,把不满的用户引导去找 Apple,而不是去留一星评价。它本身并不能批准退款,只有 Apple 才能做到这一点。 Apple 支持的退款申请指南介绍了客户会看到的具体内容。

这一点值得明确说清楚,因为 App Store 退款管理常常被过度宣传:你无法批准或拒绝一笔退款。你能做的,是在 Apple 做决定的过程中向其提供证据,并表明一个 preference。这就是你能施加的全部影响力,而且已经比大多数开发者实际用到的要多了。

参考来源

常见问题

可以。这正是 CONSUMPTION_REQUEST 通知和 Send Consumption Information 接口存在的目的。你无法批准或拒绝退款,但 Apple 会把你发送的信息作为决策的参考依据之一。

接收 App Store Server Notifications V2,验证 JWS 签名,把交易匹配到对应用户,检查是否已获得同意,整理出真实的消耗数据,并在 12 小时内将其 PUT 到 v2 consumption 接口。剩下的工作就是幂等性处理、告警和记录保存。

需要在 Users and Access 下的 Integrations 中获取的 In-App Purchase key。你的 App Store Connect API key 无法通过这些调用的身份验证,你会收到一个看起来像是时钟出问题的 401 错误。

不是必须的,这是可选项。但如果 deliveryStatus 不是 DELIVERED,那么这个值必须为 0;而对于按比例计算的自动续期订阅,你必须完全省略这个字段。

不要回应。Apple 在这一点上的指引非常明确,而且同意无法在请求到达之后再补充获取。

它只是一个参考输入,不是保证。Apple 表示会综合考虑多种因素。实际情况是,对于一笔消耗较多的购买,一个证据充分的 DECLINE 回应被驳回的概率,远高于那些根本没人回应的请求。

#:Google Play chargeback#Google Play chargeback handling#Chargeback automation#Google Play refunds#App revenue protection#Chargeback management for apps
Refund SensorRefund Sensor TeamRefund defense for App Store and Google Play developers