PAYATHON 2026

所有用户在免费试用结束后进入 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 配置

逐项确认以下内容:

  1. 订阅产品状态正常,并且已经获准销售。
  2. 自动续期订阅位于正确的 subscription group。
  3. 价格档位、基础价格及各 storefront 的可售范围符合预期。
  4. introductory offer 类型确实为 Free Trial,期限是预期的 3 天。
  5. Paid Applications Agreement、银行信息和税务信息都处于有效状态。
  6. 没有意外的订阅价格调整,也不存在需要用户同意的价格上涨。
  7. 生产环境中的 product ID 与代码和后端配置完全一致。
  8. Sales and Trends 中查看的是生产数据,并且已经正确处理报表时区和延迟。
  9. 如果启用了 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,但应避免保存不必要的个人信息。

建议的排查顺序

  1. 从服务器侧抽取一批受影响的 originalTransactionId。
  2. 验证这些订阅确实来自生产环境,并确认产品、storefront 和首次续订日期。
  3. 通过经过验证的服务器通知或 App Store Server API 核对真实状态。
  4. 分别统计进入重试后恢复、仍在重试和最终过期的比例。
  5. 按国家、campaign、publisher 和注册行为拆分失败率。
  6. 检查 App Store Connect 中的订阅配置、协议、税务和价格状态。
  7. 使用真实生产账户进行小规模端到端验证。Sandbox 的续订节奏和支付行为不能代表生产环境。
  8. 向 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 针对具体交易调查支付环节。

备注:内容仅供参考。