我因为 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 退款请求自动化
按照我会采用的顺序,整条流程大致如下:
在 App Store Connect 的 App Information 下配置 Production Server URL,并选择 Version 2。Consumption request 不会通过 V1 传输。
在信任任何一个字段之前,先对照 Apple 的根证书验证 JWS 签名链。
把交易匹配到对应用户。如果你在购买时附加了 appAccountToken,这是最快的路径。如果没有,你就得按商品和日期反向查找,这很快就会变得混乱。
检查是否已获得同意。没有同意,就不要回应。记录下来,然后继续处理下一个。
获取真实的使用数据,不是估算值,也不是默认值。如果你回答不了“客户用了多少”这个问题,那才是你自动化流程中真正的缺口。
构建 ConsumptionRequest,并套用上面提到的两个限制条件。
把它 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 回应被驳回的概率,远高于那些根本没人回应的请求。



