所有用户在免费试用结束后进入 Billing retry 状态
结论
Billing Retry 表示免费试用已经到期,Apple 尝试续订时未能完成扣款,目前仍在重试。这不代表用户主动取消,也不等于订阅已经永久失效。
如果 500 名试用用户中约有 90%~95% 进入该状态,这个比例显然需要调查,但仅凭 Sales and Trends 不能断定 Apple 的支付系统发生了故障。常见原因包括:
- 广告带来了低质量用户、激励流量或利用免费试用套利的用户。
- 用户所在市场的支付方式失败率较高。
- 用户开始试用后移除了支付方式、账户余额不足,或者支付方式需要重新验证。
- 订阅价格、销售地区、协议或税务配置异常。
- 后端混淆了
Billing Retry、宽限期和订阅过期,导致指标解读有误。 - 报表统计口径、时区或数据延迟使短期比例失真。
正常情况下,App 代码无法决定 Apple 是否对续订扣款,也不能主动将某次续订设置为 Billing Retry。只要用户通过 App Store 创建了有效订阅,后续扣款就由 Apple 处理。因此,更值得检查的是用户支付能力、流量质量、App Store Connect 配置和数据统计方式,而不是购买按钮本身的实现。
为什么不是 Cancelled
自动续期订阅有几种容易混淆的状态:
- 用户关闭自动续期:当前权益通常会持续到已付费周期或试用期结束,之后才会过期。
- 续订扣款失败:订阅进入
Billing Retry,Apple 可能继续尝试恢复订阅。 - 启用了 Billing Grace Period 且订阅满足条件:扣款失败后,用户可以在宽限期内暂时保留权益。
- 扣款最终未恢复:订阅过期,用户失去权益。
- 扣款恢复:系统产生新的有效续订交易,订阅重新生效。
因此,Billing Retry 不能证明银行卡一定无效。支付授权、账户余额、付款方式验证或商店账户状态等问题,都可能造成暂时性的续订失败。
优先检查流量质量
两轮广告使用了不同的”来源”,不代表两批用户真正独立。如果广告平台、代理商或下游流量网络存在重叠,仍然可能带来相似的低质量流量。
建议按以下维度拆分数据:
appAccountToken对应的内部用户及其注册时间。- 广告平台、campaign、ad set、creative 和 publisher/sub-publisher。
- storefront、国家或地区。
- 设备、系统版本和语言。
- 免费试用开始时间及首次续订时间。
- 进入
Billing Retry后是否恢复扣款。 - 退款、撤销、重复账户和设备复用情况。
- 从安装到开始试用的时间,以及试用期间真实的产品使用行为。
尤其需要留意以下信号:
- 大量用户安装后几分钟内就开始试用。
- 同一设备、网络或相似账户出现批量注册。
- 用户在试用期内几乎没有使用核心功能。
- 扣款失败集中在某些地区或 publisher。
- 广告平台报告的订阅数与 App Store 交易数相差很大。
如果用户注册只是为了完成激励任务或领取免费权益,他们可能会使用余额不足、无法持续扣款或随后被移除的支付方式。即使广告来源名称不同,两轮推广也可能出现相同结果。
检查 App Store Connect 配置
逐项确认以下内容:
- 订阅产品状态正常,并且已经获准销售。
- 自动续期订阅位于正确的 subscription group。
- 价格档位、基础价格及各 storefront 的可售范围符合预期。
- introductory offer 类型确实为
Free Trial,期限是预期的 3 天。 - Paid Applications Agreement、银行信息和税务信息都处于有效状态。
- 没有意外的订阅价格调整,也不存在需要用户同意的价格上涨。
- 生产环境中的 product ID 与代码和后端配置完全一致。
- Sales and Trends 中查看的是生产数据,并且已经正确处理报表时区和延迟。
- 如果启用了 Billing Grace Period,确认其环境和期限设置符合业务预期。
协议、税务或银行资料异常通常会影响收款或产品销售资格,但在没有证据时,不应直接认定这些问题会使所有用户进入 Billing Retry。判断时应结合 App Store Connect 显示的具体警告和 Apple 返回的信息。
核对购买代码
购买代码通常不会导致 Apple 的续订扣款失败,但实现错误可能造成重复购买、归因错误,或者客户端显示的状态与服务器状态不一致。
StoreKit 2 的基本处理应包括交易验证和 finish():
import StoreKit
func purchase(_ product: Product, appAccountToken: UUID) async throws {
let result = try await product.purchase(
options: [.appAccountToken(appAccountToken)]
)
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await updateEntitlement(using: transaction)
await transaction.finish()
case .pending:
// 等待 Ask to Buy 或其他待处理流程完成
break
case .userCancelled:
break
@unknown default:
break
}
}
func checkVerified<T>(
_ result: VerificationResult<T>
) throws -> T {
switch result {
case .verified(let value):
return value
case .unverified:
throw PurchaseError.failedVerification
}
}
enum PurchaseError: Error {
case failedVerification
}
还需要持续监听后续交易,不能只处理首次购买:
func observeTransactions() -> Task<Void, Never> {
Task.detached {
for await result in Transaction.updates {
guard case .verified(let transaction) = result else {
continue
}
await updateEntitlement(using: transaction)
await transaction.finish()
}
}
}
finish() 是交易队列处理流程的一部分。遗漏它会导致交易被重复投递,但通常不会使 Apple 的续订扣款进入 Billing Retry,所以不能把它当作这个问题的主要原因。
不要只依赖客户端状态
生产环境应使用 App Store Server Notifications V2,并在必要时通过 App Store Server API 获取订阅状态。服务器通知需要验证 JWS 签名,不能直接信任客户端上报的数据。
后端至少应区分以下情况:
- 首次购买成功。
- 正常续订。
- 续订失败并进入重试。
- 续订偏好或自动续期状态发生变化。
- 宽限期结束。
- 订阅过期。
- 扣款恢复。
- 退款或撤销。
处理逻辑可以抽象为:
verified transaction exists and is active
-> grant entitlement
billing retry with active grace period
-> keep entitlement until grace period expires
billing retry without active grace period
-> determine entitlement from verified subscription status
expired or revoked
-> remove entitlement
recovered renewal
-> restore entitlement and record recovery
通知名称和具体字段应以当前 App Store Server Notifications V2 文档及实际解码结果为准。不要只根据某个通知名称授予或撤销权益,而应综合判断经过验证的交易、订阅状态、expiresDate、撤销信息和宽限期信息。
验证报表统计口径
在判断”95% 进入 Billing Retry”之前,必须先明确分母和观察窗口。例如:
- 分母是所有开始试用的用户,还是所有已经到达首次续订日期的用户?
- 尚未结束试用的用户是否被计入分母?
- 同一用户的多次重试事件是否被重复计数?
- 数据是按订阅事件统计,还是按唯一订阅用户统计?
- 用户在观察窗口结束后恢复扣款,是否仍被算作失败?
- 不同国家、日期和订阅产品是否被混在同一组中?
建议按唯一 originalTransactionId 建立转化漏斗:
开始免费试用
-> 到达首次续订日期
-> 首次扣款成功
-> 进入 Billing Retry
-> 后续恢复
-> 最终过期
originalTransactionId 适合用来关联同一订阅生命周期。transactionId 标识的是具体交易,不能直接用于统计唯一订阅用户。内部用户归因可以结合 appAccountToken,但应避免保存不必要的个人信息。
建议的排查顺序
- 从服务器侧抽取一批受影响的
originalTransactionId。 - 验证这些订阅确实来自生产环境,并确认产品、storefront 和首次续订日期。
- 通过经过验证的服务器通知或 App Store Server API 核对真实状态。
- 分别统计进入重试后恢复、仍在重试和最终过期的比例。
- 按国家、campaign、publisher 和注册行为拆分失败率。
- 检查 App Store Connect 中的订阅配置、协议、税务和价格状态。
- 使用真实生产账户进行小规模端到端验证。Sandbox 的续订节奏和支付行为不能代表生产环境。
- 向 Apple 提交包含具体标识符和时间范围的工单。
提交工单时,不要只写”90% 扣款失败”,而应提供经过脱敏的诊断材料:
- app Apple ID 和 bundle ID。
- subscription product ID。
- 受影响的 storefront。
- 精确的 UTC 时间范围。
- 若干
originalTransactionId及相关transactionId。 - 对应的服务器通知类型和时间。
- App Store Server API 返回的状态摘要。
- 分母定义、唯一订阅数和恢复率。
- 两轮推广按地区及渠道拆分后的比较。
这些信息可以帮助 Apple 定位具体交易,避免工单再次只收到通用文档。
注意事项
收到 Billing Retry 后,不要立即在数据库中永久取消订阅,也不要自行调用支付接口重试扣款。App Store 订阅的续订和账单恢复由 Apple 管理。
也不要把”进入重试”直接解释成”Apple 支付系统异常”。如果多个广告批次来自相似地区、受众或下游流量网络,高失败率可能反复出现。只有排除了统计错误、流量质量和配置问题,并且真实生产订阅在多个独立市场持续出现相同异常后,才有充分理由要求 Apple 针对具体交易调查支付环节。
备注:内容仅供参考。
