App Store Refund Management

如何为 Apple 退款请求提交消费信息

了解如何为 Apple 退款请求提交消费信息,并了解 Apple 退款自动化如何让退款管理更快、更轻松。

5 min read
如何为 Apple 退款请求提交消费信息

如果你曾接触过 Apple 的退款流程,大概在文档中见过那个 12 小时响应窗口的说法。但文档并不总能说清楚:具体该如何响应。

CONSUMPTION_REQUEST 是一个通知;而对它的回应,则是对另一个 API 的调用,这个 API 有自己的密钥,其请求体结构自 2021 年以来已经变化过两次。我们之前写过一篇文章,介绍了 这条通知本身包含哪些内容。本文要讲的,正是那之后的部分——真正需要你去回应的环节。

核心要点

  • 这其实就是对 App Store Server API 发出一次 PUT 请求。整个机制就是如此。

  • 每个请求都用你的应用内购买密钥生成的 JWT 签名;超过 60 分钟的令牌会被拒绝。

  • v2 版本的请求体包含五个字段,其中三个是必填项。

  • Apple 只会返回 202,仅此而已。退款的最终决定会在之后以另一条独立通知的形式送达。

  • 从通知送达的那一刻起算,你有 12 小时的时间,并且可以在这个窗口内重新发送。

什么是 Apple 消费请求?

抛开那些专业术语,Apple 消费请求本质上就是一个问题。某位客户向 Apple 申请了退款,而在给出答复之前,Apple 会先向你的服务器询问:这个人是否拿到了他们所购买的东西,又是否使用过?

Apple 会把你提供的信息,与它已经掌握的其他信息(例如这位客户申请退款的频率)结合起来,自行做出判断。

很多人在这一点上理解错了。你并不负责批准或拒绝退款,你只是提供事实。

Apple 发出的 CONSUMPTION_REQUEST,会以 version 2 服务器通知的形式,送达你在 App Store Connect 中设置的服务器 URL。

2021 年时,它只会在消耗型商品的场景下触发。2024 年加入了订阅,到 2025 年 12 月,Apple 发布了该端点的 v2 版本,其文档称这一版本覆盖所有产品类型,包括非消耗型商品和非自动续期订阅。通知参考页面目前仍只列出最初的两种类型;我建议相信更新的那份文档,按全部四种类型来构建方案。

说到命名问题:很多人搜索“Apple refund API”,但其实并不存在这样一个独立的 API。这个调用属于 App Store Server API 的一部分。

提交之前你需要准备什么

这四项中有三项是急不来的,所以请在你第一次发起请求之前先读完这部分,而不是之后再看。

第一项是密钥,具体来说是在 App Store Connect 的 Users and Access 下创建的应用内购买密钥。Apple 只会给你一次 .p8 文件;此外你还需要这个密钥的 key ID,以及你的 issuer ID。

第二项,是某种能把交易 ID(Apple 给你的信息)映射到你数据库中具体某个人(你真正需要的信息)的方式。比较干净的做法是使用 appAccountToken,在购买时设置好,之后与该交易相关的每一条通知都会带回这个值。

如果这个词对你来说还很陌生,建议先读一读 为什么 appAccountToken 对退款申诉如此重要这篇文章,因为没有这层映射关系,本文接下来的内容就只是纸上谈兵。

第三项,也是那种凌晨两点出问题时根本来不及补救的一项:客户同意(consent)。

Apple 的文档明确说明:如果客户没有同意你分享这些数据,你就不应该作出任何响应。文档也用更偏法律的措辞指出,什么才算有效的同意取决于你的运营所在地,而他们无法就此给出建议。

通常的做法是在你的服务条款中加入明确的授权(opt-in)条款,而且这段文字必须在请求出现之前就已经存在。

第四项是交易 ID 本身,你只能在通知到达、并且验证了 Apple 的签名之后才能拿到它。

如何一步步提交消费信息

大多数搜索“send consumption information apple”的人,最终都会跳转到 Apple 的参考文档页面,那里列出了各个字段,然后戛然而止。下面是实际操作中的具体流程。

  1. 第一步,先确认这条通知是否真实可信。signedPayload 是一个 JWS;Apple 的 App Store Server Library 会对其进行验证,并返回解码后的交易信息,其中包含 transactionId 和 appAccountToken。

  2. 在你这边查找这笔购买记录。你需要弄清楚:我们是否已交付商品、大致使用了多大比例、客户事先是否看过样品或试用版,以及我们希望 Apple 怎么处理这笔退款。

  3. 接下来是生成令牌。Header 部分:alg 为 ES256,kid 为你的 key ID,typ 为 JWT。Payload 部分:iss 为你的 issuer ID,iat 为当前时间,exp 最多比 iat 晚 60 分钟,aud 是字符串字面值 appstoreconnect-v1,bid 为你的 bundle ID。Apple 的 生成 JSON Web Token 指南给出了确切的结构,不过这个库会帮你自动生成。

  4. 发出 PUT 请求。生产环境的 URL 是 https://api.storekit.apple.com/inApps/v2/transactions/consumption/{transactionId},将 JWT 作为 Bearer token 放入 Authorization 头,请求体大致如下:

{

"customerConsented": true,

"deliveryStatus": "DELIVERED",

"consumptionPercentage": 94000,

"sampleContentProvided": false,

"refundPreference": "DECLINE"

}

  1. Apple 会返回 202 Accepted,仅此而已。请记录下时间、请求体和交易 ID,因为一个月后总会有人问起:我们当时到底告诉了 Apple 什么。

  2. 然后就是等待。最终决定会以 REFUND 或 REFUND_DECLINED 通知的形式送达。如果是 REFUND,请在当天就撤回对应的内容或权益。

所以,如果你一直在想“如何提交 Apple 真正会采用的消费信息”,答案就是这样:一次请求,五个字段,在时间窗口内完成。

Apple 还允许你在这 12 小时内发送更新后的负载,所以如果第一次调用时信息并不完整,可以再发第二次。

有几件事字段名本身并没有说明。consumptionPercentage 的单位是千分之一,这一点很容易让人踩坑:94000 表示 94%,100000 表示全部用完;而如果 deliveryStatus 不是 DELIVERED,这个值就必须为 0。

表示“未交付”的选项包括 UNDELIVERED_QUALITY_ISSUE、UNDELIVERED_WRONG_ITEM、UNDELIVERED_SERVER_OUTAGE 和 UNDELIVERED_OTHER。

refundPreference 是可选字段,可取值 GRANT_FULL、GRANT_PRORATED 或 DECLINE;如果是针对自动续期订阅使用 GRANT_PRORATED,不需要填写具体比例,Apple 会根据剩余时间自行计算。

The Send Consumption Information 参考文档中列出了完整的错误列表。

响应代码分别意味着什么

只有一种响应算是“正常”的,那就是 202。其他所有响应,基本都是 Apple 在明确告诉你哪里出错了。

400 系列的错误最有参考价值,因为响应体会直接写明具体的错误名称。

ConsumptionPercentageOutOfRangeError 几乎总是意味着有人把 94000 误写成了 94。

UndeliveredConsumptionPercentageNonZeroError 意味着你一边说商品从未送达,一边又声称它已经被部分使用。

InvalidCustomerConsentedError 这个名字已经说明了一切。

401 问题出在令牌本身:密钥错误、过期时间超过一小时、bundle ID 不匹配,或者服务器时钟偏差太大,导致 iat 看起来像是未来的时间。

404 表示没有找到这个交易 ID,我每次遇到这种情况第一个会怀疑的,就是是否把沙盒交易发到了生产环境的 URL(或者反过来)。

429 是速率限制(生产环境中每个应用每秒 50 次请求,沙盒环境是这个数字的十分之一);在重试之前,请先查看 Retry-After 响应头。

500 问题出在 Apple 那一端;稍后再试即可。

如何在沙盒环境中测试

测试这个功能其实比听起来简单,只有一个需要注意的地方。

在 App Store Connect 中填入沙盒服务器 URL,使用沙盒 Apple Account 登录,购买一个商品,然后打开你应用中的退款弹窗(beginRefundRequest)并提交一次退款请求。

Apple 的 退款请求测试指南说明了测试环境的具体行为:选择任意理由,退款都会自动获批;选择 Other 并输入 DECLINE,退款就会被拒绝;输入 GRANT_PRORATED,则会得到按比例退款的结果。

需要注意的地方就在这些规则里:沙盒环境是根据你在弹窗里输入的内容来做判断的,而不是你服务器发送的数据,所以它并不能告诉你,你提交的数据是否真的能说服 Apple。

它能测试的是底层机制:通知能否送达,你对 api.storekit-sandbox.apple.com 发出的 PUT 请求能否返回 202,以及 REFUND 或 REFUND_DECLINED 通知出现时能否被正确处理。

另外,沙盒环境不会发送退款邮件,所以不要等着收邮件。

Apple 退款自动化:什么时候该停止手动处理

如果靠人工处理,这项工作就意味着要有人全天候盯着警报、随时查询数据,并在 12 小时倒计时结束前填好这五个字段并发送出去。

如果每月只有少数几起 Apple 退款请求,这样做没什么问题。但一旦遇上没人盯着的长假,情况就会明显变得不妙。

要实现 Apple 退款请求的自动化处理,你需要把上面提到的所有环节走一遍,并把“人”从中间去掉:通知到达后,你的规则会把交付和使用数据转换成负载,调用会在几分钟内发出,决定会记录在对应案例上,而 REFUND 则会触发权益撤回。

构建这套流程本身并不是什么大工程;真正持续消耗成本的,是之后要不断跟上 Apple 的各种变化(比如 V1 切换到 v2 这种事)。

如果你不想自己搭建,我们在 Apple 退款管理工具对比一文中梳理了专门为此打造的各种服务,也谈到了 Refund Sensor 适合哪些场景、又不适合哪些场景。

每次听到这个说法,我都想纠正一下:Apple 退款自动化改变的,是你能否可靠地做出响应,而不是谁来做最终决定。这个 API 里的任何部分,都不会把最终决定权交到开发者手里。

结语

这个调用本身很简单:一个签名令牌,一次 PUT 请求,五个字段,一个需要记录的 202。

真正决定成败的,是别的地方:是否已经在服务条款中写好了同意条款,每笔购买是否都带有 appAccountToken,使用记录能否在几秒内查到,以及通知在凌晨三点到达时,是否有系统会随之被唤醒。

把这些都处理妥当,这件事就不再是一场手忙脚乱的应急,而会变成你的服务器自然而然就能完成的日常工作。

常见问题

这是 App Store Server API 中的一个端点,用于响应 CONSUMPTION_REQUEST。你发送一个包含消费详情的 PUT 请求,Apple 会返回 202 Accepted。

从 CONSUMPTION_REQUEST 到达起,你有 12 小时的时间。Apple 允许你在这同一个 12 小时窗口内提交更新后的负载。

可以。Apple 允许你在这 12 小时窗口内发送更新后的负载。当第一次提交的信息不完整时,这一点很有用。

不是。202 Accepted 只表示 Apple 已收到你提交的信息。最终结果会在之后以 REFUND 或 REFUND_DECLINED 通知的形式送达。

是的。提交消费信息时,customerConsented 字段是必填项。如果客户尚未同意,Apple 的文档要求你不要作出响应。

#Apple Refund Automation#Apple Refund Request#Apple App Refund#Consumption Information#Apple Billing#Refund Process
Refund SensorRefund Sensor TeamRefund defense for App Store and Google Play developers