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

为每一笔 App Store 购买附加 appAccountToken,否则你无法为退款辩护

当客户申请退款时,Apple 会向你的服务器发送一个 CONSUMPTION_REQUEST,但这笔交易从不说明他们是谁。appAccountToken 就是那个把购买关联回你的用户的 UUID。设置它,你就能用真实数据回应 Apple。跳过它,你就只能靠猜。

一把带编号的黄铜钥匙搁在深色账本上,旁边是一部显示购买收据的智能手机,展示 appAccountToken 如何将一笔 App Store 购买关联到某个用户账户

要点

  • appAccountToken 是你附加到 App Store 购买上的一个 UUID,让最终生成的交易指回你自己系统中确切的那个用户。Apple 会把它存储在交易上,并在该交易出现的任何地方将其返回。
  • Apple 强制执行的唯一格式规则是:该值必须是有效的 UUID。传入其他任何东西,一个 id、一个邮箱、一串拼接的字符串,StoreKit 都会悄悄丢弃它,并把 appAccountToken 返回为 nil。
  • 在 StoreKit 2 中,你用一个购买选项 Product.PurchaseOption.appAccountToken(_:) 来设置它,使用你为该账户生成并存储的一个稳定 UUID。
  • 只需在原始购买时设置一次,Apple 就会在订阅链中的每一次续订、扣款重试和升级中携带同一个令牌。
  • 自 2025 年起,Set App Account Token 端点让你的服务器能够为在你的 App 之外完成的购买附加令牌,比如优惠代码兑换和推广购买,这些是应用内流程永远无法触及的。
  • appAccountToken 正是让 Apple 的 CONSUMPTION_REQUEST 变得可回应的东西。没有它,你就无法在 12 小时窗口内把退款映射到那个你本应描述其使用情况的客户。
  • 一笔你无法识别的退款,就是一笔你无法辩护的退款。你会把钱退还给那些你本有证据可以留住的购买,还搭上你为交付它们已经花掉的算力、API 调用和分成。

客户向 Apple 申请退款,Apple 向你的服务器发送一个 CONSUMPTION_REQUEST,而你有十二个小时用真实数据回答这个人是如何使用产品的。然后你打开通知,才意识到你根本不知道他们是谁。这笔交易带着一个 originalTransactionId 和一个产品 id,却没有任何东西指向你自己数据库中的那个账户。这道缺口正是 appAccountToken 要填补的,而如果你在购买时没有设置它,事后你就无法为那笔销售填补它。

appAccountToken 是你附加到一笔购买上的 UUID,让最终生成的 App Store 交易带上一个指回你系统中确切用户的指针。设置它,Apple 日后就那位客户提出的每一个退款问题,都会带着他们的身份而来。跳过它,你就只能靠猜。下面讲的是这个字段是什么、如何设置它、那个拯救 App 之外购买的新端点,以及当退款到来时这道缺失的关联究竟要付出什么代价。

appAccountToken 到底是什么

appAccountToken 是一个不透明的 UUID,由你生成并在购买那一刻传给 StoreKit。Apple 会把它存储在交易上,并在那笔购买的交易信息中返回,而且它会一直留在那里。用 Apple 的话说,它是「将交易与用户在你自己服务上的账户关联起来的 UUID」。唯一的格式规则是它必须是一个 UUID。Apple 不会读取它,不会验证它映射到什么,也不在意它在你这边意味着什么。它是一条由你掌控的关联。

因为它存在于交易之上,所以交易出现在哪里,它就会跟着回到哪里。服务器通知中签名的交易、App Store Server API 的 Get Transaction Info 响应,以及订阅链中的每一次续订,只要你在原始购买时设置了它,就都会携带同一个令牌。一个 UUID,只附加一次,便在这段关系的整个生命周期中跟随着客户的账单。

它必须是一个真正的 UUID,否则会悄无声息地消失

Apple 强制执行的唯一规则是格式。StoreKit 2 要求一个 RFC 4122 UUID。如果你传入一串拼接的字符串、一个整数 id 或一个邮箱地址,StoreKit 不会抛出错误。它会丢弃这个值,交易返回时 appAccountToken 被设为 nil。开发者不断踩到这个坑,而症状总是相同,在 Apple 自己的论坛上以「appAccountToken is missing in the transaction payload」的某种变体出现,几乎总是因为传入的值不是有效的 UUID。在服务器端生成一个真正的 UUID,将它与账户对应存储,永远不要给 StoreKit 别的东西。

如何在购买时设置它

在 StoreKit 2 中,它是单个购买选项。在用户注册或首次进入结账时在你的服务器上生成 UUID,把它存储在他们的账户记录上,并把同一个值传入购买调用。

其签名是 Product.PurchaseOption.appAccountToken(_ token: UUID),而一笔购买看起来像 try await product.purchase(options: [.appAccountToken(token)])。当交易返回时,无论是通过 App Store Server API 验证的,还是由服务器通知送达的,它都会携带那个 UUID,而你的服务器用一次查询就能找到这位客户。

每个账户使用一个稳定的令牌

不要为同一用户的每一笔购买生成一个新的令牌。Apple 会在同一条链中的续订、扣款重试和升级中返回该令牌,所以每个账户一个稳定的 UUID,会给你一条从首次购买贯穿至未来每一个事件的清晰线索。一个每笔购买都变化的令牌会切断这条线索,并使整个目的落空。一个账户,一个令牌,每次该账户购买时都复用它。

拯救 App 之外购买的那个端点

直到 2025 年之前一直有个漏洞。如果客户兑换了优惠代码,或直接从 App Store 购买了一个推广的应用内购买项,你的 App 从未运行过购买流程,所以没有任何地方可以设置 appAccountToken。那些交易到来时是匿名的,而且一直保持匿名。

WWDC 2025 用 Set App Account Token 端点填补了这道缺口。你的服务器在 App Store Server API 上调用 PUT /inApps/v1/transactions/{originalTransactionId}/appAccountToken,请求体中带上 UUID,Apple 便会在那笔交易上设置令牌。它适用于每一种产品类型,涵盖优惠代码兑换和推广购买,而你发送的值会覆盖交易上已有的任何令牌。现在你可以事后从你的服务器为一笔购买建立关联,而无需它经过你的 App。

深色书桌上堆着一叠纸质销售收据,客户姓名一栏留空,其中一张被暖色聚光灯照亮,展示一笔到来时没有 appAccountToken 来识别买家的 App Store 购买
购买渠道你在哪里设置 appAccountToken备注
应用内购买购买时的 StoreKit 购买选项Product.PurchaseOption.appAccountToken(UUID)
优惠代码兑换Set App Account Token 端点,服务器端没有应用内流程可挂接,所以事后设置
来自 App Store 的推广应用内购买Set App Account Token 端点,服务器端购买发生在你的 App 之外
订阅续订无需操作自动从原始购买中沿用

缺失的关联在哪里让你损失金钱

令牌的意义不在于整洁的记录。而在于 Apple 面向开发者的那个退款问题,也就是 CONSUMPTION_REQUEST,只有当你能找到它所关乎的那位客户时才可回答。

当买家就一个消耗型项目或一个非续订订阅申请退款时,Apple 会向你的服务器发送一个 CONSUMPTION_REQUEST 通知,并给你十二个小时用 Send Consumption Information 回复。你的回答是关于那位具体客户的数据:他们消耗了多少产品、他们的账户存续时长、他们的终身消费、他们的交付状态。appAccountToken 本身就是那个请求中的字段之一,更关键的是,它正是通知中的交易映射到那个你即将描述其使用情况的账户的方式。没有令牌,就没有查询,就没有准确的回答。

一份空白的回答究竟要付出什么代价

一笔无法识别的退款迫使你做出糟糕的选择。你可以用空白来回答这个消耗信息请求,而这会被解读为低消耗,并推动 Apple 倾向于批准退款,包括批给那些大量使用了产品的客户。或者你可以靠猜。无论哪种方式,你都是在退还那些你本有证据可以辩护的购买,而你早已为服务它们付出了真实的成本。

那个成本不是销售价格。一个运行了一批模型 API 调用、生成了图片、导出了视频或触发了创作者分成的消耗型项目,在它被交付的那一刻就花掉了真金白银。退款退还的是客户的付款。它不会退还供应商的账单。把一位无法识别的客户乘以他们提交的每一笔退款,再乘以每一个指望你不知道他们是谁的连环退款者,你跳过的那个令牌就成了你从未写下的最昂贵的一行代码。

同样的理念在 Android 上也存在,只是换了个名字

Google Play 用 setObfuscatedAccountId 解决同一个问题,它把一个账户标识符附加到购买上,这样 Google 通过 orders.reviewrefund 进行的拒付审查就能对着一个真实的用户来回答。不同的商店,不同的机制,同样的教训:在购买时附加身份,否则你日后无法为争议辩护。在 App Store 上,那个工具是 appAccountToken,而且它必须是一个 UUID。

让令牌保持到位的三个习惯

  • 为每个账户生成一个 UUID 并存储它。每位客户一个稳定的令牌,在他们注册时或首次结账时创建,保存在他们的记录上,并在每一笔购买时复用。
  • 在传入之前先验证它。在你的购买代码中确认该值是一个真正的 UUID,这样一个格式错误的 id 就永远不会悄悄变成交易上的 nil 令牌。
  • 为 App 之外的购买补写令牌。当一个服务器通知就一次没有令牌的优惠代码兑换或推广购买而到来时,调用 Set App Account Token 端点来附加正确的那个。

做到这三点,Apple 日后发给你的每一笔交易,包括退款请求,都会到来时就已经与它所属的那位客户绑定好了。

这就是 RefundHalt 所依赖的基础工作。我们从每一笔交易和服务器通知中读取 appAccountToken,把它与我们已经为该账户追踪的使用情况关联起来,并在十二小时窗口内用客户的真实数字回答 Apple 的消耗信息请求。令牌就是那条线索。设置它一次,你的退款辩护就有了可以抓住的东西。

常见问题解答

App Store 中的 appAccountToken 是什么?
appAccountToken 是你生成并通过 StoreKit 附加到一笔购买上的 UUID,让最终生成的 App Store 交易指回你自己系统中某个特定的用户账户。Apple 会把它存储在交易上,并在交易信息、服务器通知以及同一条链中的每一次续订里返回它,这让你能够把任何未来的事件,包括退款请求,关联到正确的那位客户。
为什么我的 appAccountToken 是 nil 或缺失?
几乎总是因为你传入的值不是有效的 UUID。StoreKit 2 要求一个 RFC 4122 UUID,并会悄悄丢弃其他任何东西,所以一串拼接的字符串、一个整数 id 或一个邮箱会返回一个 nil 的 appAccountToken,而购买仍然成功。另一个常见原因是在你的 App 之外完成的购买,比如优惠代码兑换,那里没有运行任何应用内流程来设置令牌。
我可以在购买之后设置 appAccountToken 吗,比如为优惠代码?
可以,自 2025 年起。App Store Server API 上的 Set App Account Token 端点让你的服务器能够通过调用 PUT /inApps/v1/transactions/{originalTransactionId}/appAccountToken 并在请求体中带上 UUID,为一笔已有的交易附加或覆盖令牌。它适用于每一种产品类型,并且专为在你的 App 之外完成的购买而设计,比如优惠代码兑换和推广的应用内购买。
appAccountToken 必须是一个 UUID 吗?
是的。Apple 强制执行的唯一格式规则是该值必须是有效的 UUID。除此之外它是不透明的,所以它可以映射到你这边任何你喜欢的账户键,但如果它不是 UUID,StoreKit 就不会存储它,交易返回时 appAccountToken 会被设为 nil。
appAccountToken 如何帮助处理退款?
当客户申请退款时,Apple 会发送一个 CONSUMPTION_REQUEST,并给你十二个小时用关于那位具体买家的数据来回应。appAccountToken 是你把通知中的交易匹配到那个你需要报告其使用情况的账户的方式,而且它本身就是消耗信息请求中的字段之一。没有它,你就无法用真实的使用情况来回答,于是 Apple 会倾向于批准那些你本有证据可以争辩的退款。
appAccountToken 应该为每一笔购买都不同吗?
不。每个账户使用一个稳定的 UUID,并在该用户的每一笔购买中复用它。Apple 会在订阅链中的续订和升级中携带该令牌,所以一个稳定的令牌会给你一条随时间推移的清晰关联。一个每笔购买都变化的令牌会切断这条关联,并使把交易关联回同一位客户变得更困难。

来源和延伸阅读

RefundHalt

App Store 和 Google Play 退款自动驾驶

继续阅读

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

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