做过 App Store 订阅服务端验证的团队,迟早会遇到 EXPIRED 通知。但 EXPIRED 只是告诉我们订阅结束了,真正决定后续运营动作的是 data.signedRenewalInfo 中的 expirationIntent。忽略这个字段,很容易把“用户主动取消”和“扣费失败”混为一谈,续订引导也会投错方向。

在 App Store Receipt 的 pending_renewal_info 中,这个字段写作 expiration_intent;而在 App Store Server Notifications V2 和 App Store Server API 里,它出现在 JWS renewal info 中,命名为 expirationIntent。两者含义一致,都是苹果对“为什么这次订阅没能续订成功”给出的原因码。
一、expiration_intent 字段出现在哪里,不同取值代表什么
App Store Server Notifications V2 的通知体是一个 JWS(JSON Web Signature)。服务端先用苹果提供的根证书完成验签,然后解开 signedPayload,得到包含 notificationType、subtype 和 data 的 payload。其中 data.signedTransactionInfo 是交易信息,data.signedRenewalInfo 是续订信息。只有后者里才有 expirationIntent。
以下是一个典型的 V2 renewal info 解码后的结构:
{
"autoRenewProductId": "com.example.premium.monthly",
"productId": "com.example.premium.monthly",
"autoRenewStatus": 0,
"expirationIntent": 2,
"gracePeriodExpiresDate": "2025-06-15T00:00:00Z",
"isInBillingRetryPeriod": true,
"signedDate": 1717550000000
}
这里 expirationIntent 的值为 2,表示账单问题。苹果官方定义的取值如下表:
| 取值 | 含义 | 对业务的影响 |
|---|---|---|
| 1 | 用户主动取消续订 | 订阅自然到期,用户流失意向明确 |
| 2 | 账单错误或支付方式无效 | 可能处于宽限期或扣费重试阶段,可挽回 |
| 3 | 用户未同意涨价 | 对价格敏感,需要解释新价值或提供优惠 |
| 4 | 商品在续订时不可购买 | 通常是配置、地区或审核状态导致 |
| 5 | 未知错误 | 需要人工排障或联系苹果支持 |
需要特别注意的是,expirationIntent 只在 EXPIRED 或 GRACE_PERIOD_EXPIRED 等通知场景中才有业务意义。如果你收到 SUBSCRIBED 或 DID_RENEW 通知,续订动作已经成功,这个字段通常不会作为决策依据。
二、服务端如何可靠解析并映射过期原因
很多团队在接入通知时只是把原始 payload 存下来,然后依据 notificationType 做粗糙处理。这样会丢失 expirationIntent 的细分价值。更推荐的做法是在验签之后立即解析并持久化这个字段,把它作为用户持有状态机的一个输入。
以 Node.js 为例,可以这样从 data.signedRenewalInfo 里取出原因码:
const jwt = require('jsonwebtoken');
const decodedPayload = jwt.verify(signedPayload, appleRootKey, { algorithms: ['ES256'] });
const renewalInfo = jwt.verify(decodedPayload.data.signedRenewalInfo, appleRootKey, { algorithms: ['ES256'] });
function mapExpirationIntent(intent) {
switch (intent) {
case 1:
return 'user_cancelled';
case 2:
return 'billing_failed';
case 3:
return 'price_increase_rejected';
case 4:
return 'product_unavailable';
case 5:
return 'unknown_error';
default:
return 'pending';
}
}
const reason = mapExpirationIntent(renewalInfo.expirationIntent);
console.log('expiration reason:', reason);
这段逻辑的关键不是 switch 本身,而是把苹果返回的整数映射成业务可读状态。后续触达、弹窗、服务端降级策略都可以围绕这些状态展开。
另一个容易忽略的点是幂等性。苹果可能对同一次过期事件重推通知,订单号 originalTransactionId 相同但 notificationUUID 不同。解析完 expirationIntent 后,应该用 originalTransactionId + subscriptionGroupIdentifier + notificationType 做去重,避免用户收到重复的续订提醒。
三、针对不同过期原因的用户续订引导策略
把 expirationIntent 解析出来以后,离真正提升续订率还有一步:根据原因设计不同的引导动作。不同类型的用户流失,其挽回概率和沟通方式差异很大。
对于取值为 1 的主动取消,不建议立即用强推动作轰炸用户。用户在 iOS 系统里已经明确关闭自动续订,此时更适合进入低频召回队列,通过产品价值回顾、功能更新通知或专门的 win-back offer 来触达。苹果也为主动取消用户提供了重新订阅的入口,但运营动作必须克制,否则容易引发卸载。
对于取值为 2 的账单失败,策略则应完全相反:及时性最重要。因为苹果通常会在扣费失败后进入重试周期,isInBillingRetryPeriod 可能为 true,同时开启宽限期。服务端可以在收到此类通知后,通过推送或应用内 banner 提醒用户更新支付方式,并明确告知如果不在宽限期内处理,权益将在某个具体时间点暂停。这个时间可以从 gracePeriodExpiresDate 中读取。
对于取值为 3 的涨价拒绝,核心不是催续费,而是解释价值。用户可能收到了 Apple 的涨价同意请求,但选择了不同意。此时可以结合用户历史使用数据,说明新价格对应的权益变化,或者提供限时优惠、年付折扣,让价格变化看起来更合理。
取值为 4 的商品不可用,往往不是用户问题,而是配置问题。服务端应第一时间检查 App Store Connect 中该商品是否被下架、是否对当前用户所在地区不可用、是否有审核中的版本导致续订失败。确认配置无问题后,再引导用户切换到另一个可用套餐。
取值为 5 的未知错误最麻烦,不要自动给用户发送“续订失败”这类含义不清的通知。可以记录为异常事件,进入人工客服或与苹果开发者支持协同排障,同时在前端展示一个联系客服的入口。
下面的表格总结了原因码、用户意图和推荐动作:
| expirationIntent | 用户侧表现 | 推荐续订引导 |
|---|---|---|
| 1 | 系统设置中已关闭自动续订 | 低频价值召回、win-back offer |
| 2 | 未主动操作,扣费被银行/风控拒绝 | 及时提醒更新支付方式,强调宽限期 |
| 3 | 收到涨价同意请求后拒绝 | 解释权益、提供优惠或替代方案 |
| 4 | 看到续订失败但非支付原因 | 检查商品配置,切换可用套餐 |
| 5 | 无明确原因 | 异常事件排障,避免误导性通知 |
四、把 expiration_intent 纳入订阅状态机与测试
实际项目中,订阅模块通常会维护一套状态机,例如 trial、active、grace_period、expired。只有把 expirationIntent 写入状态流转条件,后续的续订引导才不会变成统一话术。
例如,当 notificationType 为 EXPIRED 且 expirationIntent 为 2 时,状态机可以先进入 grace_period 而不是直接标记为 expired;当 GRACE_PERIOD_EXPIRED 通知到来时,再执行权益回收。这样的差异能让用户在宽限期内完成支付方式修复而无需重新订阅。
测试阶段除了验证验签和字段解析,还应该用 Apple 提供的沙盒通知模拟不同过期原因。沙盒环境下可以通过创建订阅、关闭自动续订、更换无效支付方式、触发涨价等操作观察 payload 中 expirationIntent 的变化。需要特别检查中断订阅和取消订阅在沙盒里的通知时序,避免把测试结果直接等同于生产逻辑。
一旦线上有真实数据,建议按 expirationIntent 聚合统计流失原因分布。你会发现每种原因对应的留存曲线差别很大,这比单纯看“订阅到期人数”更能指导产品与运营的资源投入。
iOS内购expiration_intent订阅续订策略修改时间:2026-10-04 13:34:50