自动续期订阅在扣费失败后会进入账单重试阶段,苹果还会根据配置给用户宽限期,但这段时间一过交易就会变成 expired。客户端做 restore 时,历史过期交易虽然能查到,却不会把当前权益恢复成可用状态。要解决这个断层,App Store Server API 的延长订阅续订日期接口是一个可选方案,它允许服务端直接把过期时间在60天内的订阅向后延长,让原本已经失效的交易重新产生可访问时段。

一、为什么过期订阅容易被客户端恢复遗漏
客户端 StoreKit 里的 Restore Purchases 主要用于恢复用户此前已经购买过的非消耗型项目或自动续期订阅。但它并不能把一条已经过期的订阅重新激活。对于自动续期订阅来说,一旦续订窗口结束,交易状态变为 expired,StoreKit 返回的当前权益信息里通常不会包含继续有效的订阅项目。即使本地收据里还能看到历史交易,服务端如果只是简单验证最新一条交易,也会判定用户没有有效订阅。
更麻烦的是,苹果的订阅生命周期里存在 billing retry 和 grace period 两个阶段。扣费失败后,苹果会按周期重试扣款;如果开发者配置了宽限期,用户在宽限期内仍然可以访问内容。真正走到 expired 状态时,距离首次扣费失败可能已经过去数天甚至数周。用户这时候回来想恢复订阅,客户端几乎没有自动处理能力,只能依赖客服手动发放权限或者让用户重新购买。
App Store Server API 提供的延长订阅续订日期接口可以在服务端把已经过期的订阅向后延长。这里的恢复并不是让用户重新获得一次新的购买,而是在苹果侧把原交易的续订日期往后推。对用户来说,订阅权益恢复到可用状态;对服务端来说,只需要更新本地到期时间并记录延长原因。
在调用恢复接口之前,应先查询订阅当前状态。App Store Server API 的 Get All Subscription Statuses 接口可以返回某个原始交易 ID 对应的状态信息。请求地址为 GET /inApps/v1/subscriptions/{transactionId},响应中 data 数组包含最近交易记录,其中的 status 字段可能为 1 到 5 的整数,分别对应有效、过期、账单重试、宽限期和已撤销。只有在确认已经过期且过期时间不超过 60 天时,才建议继续调用延长接口。
二、延长订阅续订日期接口的鉴权与参数
苹果官方并没有一个名字直接叫恢复订阅的独立接口,服务端恢复订阅通常基于 Extend a Subscription Renewal Date 实现。该接口的请求地址为 POST https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/extend,沙盒环境需要将域名换成 api.storekit-sandbox.itunes.apple.com。它仅适用于自动续期订阅,不能用于非续期型购买。
所有 App Store Server API 请求都需要使用 App Store Connect 生成的 API Key 进行 JWT 鉴权。JWT 使用 ES256 算法,头部包含 kid 和 alg,载荷包含 iss、iat、exp、aud 和 bid。其中 iss 是 Issuer ID,aud 固定为 appstoreconnect-v1,bid 是应用的 Bundle ID,exp 相比 iat 最大不能超过 20 分钟。请求头里需要携带 Authorization: Bearer 生成的JWT,Content-Type 使用 application/json。
延长接口的请求体包含三个核心字段。originalTransactionId 是用户最初购买订阅时的原始交易 ID,这个值在后续所有续订中保持不变;extendByDays 是本次希望延长的天数,单次调用最多 90 天;extendReasonCode 是延长原因码,常见取值包括 0 表示其他、1 表示客户满意度、2 表示延迟计费、3 表示涨价补偿、4 表示宽限期续期。一般情况下恢复过期订阅用原因码 1 即可。
请求成功后返回 HTTP 200,响应体包含 extendedSubscriptionRenewalDate、originalTransactionId、webOrderLineItemId、success 和 statusCode。其中 extendedSubscriptionRenewalDate 是延长后的新到期时间,格式为 Unix 毫秒时间戳。服务端拿到这个值以后,应把它作为用户订阅的新到期时间写入本地数据库。
三、Python调用示例:查询状态并恢复过期订阅
下面给出一个完整的 Python 请求示例,演示如何生成 JWT、查询订阅状态,并在确认过期后调用延长接口。示例使用 PyJWT 和 requests 两个库,私钥文件可以从 App Store Connect 下载,通常是一个以 .p8 结尾的密钥文件。
代码先读取私钥并生成 20 分钟内有效的 JWT,然后带上 Bearer Token 查询交易状态。如果状态显示为 2,并且过期时间与当前时间差距小于 60 天,就可以继续调用延长接口。为了让示例清晰,这里省略了部分异常处理和日志输出,实际生产环境应补齐重试和记录。
import time
import jwt
import requests
from datetime import datetime, timezone
KEY_ID = "YOUR_KEY_ID"
ISSUER_ID = "YOUR_ISSUER_ID"
BUNDLE_ID = "com.example.app"
PRIVATE_KEY_PATH = "AuthKey_XXXXXXXX.p8"
ORIGINAL_TRANSACTION_ID = "1000000899072311"
ENVIRONMENT = "production" # sandbox or production
def read_private_key(path):
with open(path, "r") as f:
return f.read()
def create_token():
now = int(time.time())
payload = {
"iss": ISSUER_ID,
"iat": now,
"exp": now + 20 * 60,
"aud": "appstoreconnect-v1",
"bid": BUNDLE_ID,
}
headers = {
"alg": "ES256",
"kid": KEY_ID,
"typ": "JWT",
}
private_key = read_private_key(PRIVATE_KEY_PATH)
token = jwt.encode(payload, private_key, algorithm="ES256", headers=headers)
return token
def get_subscription_status(token, transaction_id):
base_url = "https://api.storekit.itunes.apple.com"
if ENVIRONMENT == "sandbox":
base_url = "https://api.storekit-sandbox.itunes.apple.com"
url = f"{base_url}/inApps/v1/subscriptions/{transaction_id}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
def extend_subscription(token, transaction_id, days=30, reason=1):
base_url = "https://api.storekit.itunes.apple.com"
if ENVIRONMENT == "sandbox":
base_url = "https://api.storekit-sandbox.itunes.apple.com"
url = f"{base_url}/inApps/v1/subscriptions/extend"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
body = {
"originalTransactionId": transaction_id,
"extendByDays": days,
"extendReasonCode": reason,
}
response = requests.post(url, headers=headers, json=body)
response.raise_for_status()
return response.json()
if __name__ == "__main__":
token = create_token()
status_data = get_subscription_status(token, ORIGINAL_TRANSACTION_ID)
print(status_data)
# 这里只做演示:真实业务里需要解析 lastTransactions 并判断 status 是否为 2
result = extend_subscription(token, ORIGINAL_TRANSACTION_ID, days=30, reason=1)
print(result)实际使用时不能把私钥文件直接打进客户端包,必须放在服务端环境变量或密钥管理系统中。JWT 的 exp 字段如果超过 20 分钟,苹果会直接返回 401 Unauthorized。调用延长接口前最好先记录原始到期时间和调用的客服工单号,便于后续审计。
如果查询状态返回的 status 已经是 1 或 4,说明订阅当前仍有效或处于宽限期,此时不需要调用延长接口,否则反而会覆盖掉原本正常的续订逻辑。只有状态为 2 且过期时间仍在 60 天以内,才是恢复接口最合适的场景。
四、恢复成功后的权限同步与Webhook兜底
延长接口调用成功只代表苹果侧生成了新的到期时间,并不等于用户本地已经自动恢复。服务端需要在收到响应后立即更新用户订阅表的到期时间和状态字段,否则客户端下次登录仍然可能看到过期。建议将接口返回的 extendedSubscriptionRenewalDate 与本地用户 ID 关联,并写入一条延长记录,方便后续排查。
除了主动调用接口后的同步,还应该监听 App Store Server Notifications V2 的 Webhook。苹果在订阅状态变化时会向开发者服务器发送 JWS 格式的通知,其中包含 notificationType 和 subtype 等字段。延长续订日期后,可能触发 DID_RENEW 或 DID_CHANGE_RENEWAL_STATUS 等通知。解析出交易信息里的 expiresDate 和 transactionId,可以与本地记录做一次幂等更新。
通知体通常如下所示,实际的 signedPayload 是一个 JWS 字符串,需要先按 JWS 规范验签并解码,再获得里面的 JSON 数据。服务端在处理通知时应使用 notificationUUID 去重,避免同一事件被重复处理导致状态错乱。
{
"signedPayload": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlGTURDQ0JBNmNB...",
"notificationType": "DID_RENEW",
"subtype": "RESUBSCRIBE",
"notificationUUID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"data": {
"signedTransactionInfo": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlGTURDQ0JBNmNB...",
"signedRenewalInfo": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlGTURDQ0JBNmNB..."
}
}如果业务侧没有维护 Webhook,恢复后的订阅到期时间仍然可以在客户端下一次启动时通过服务端查询接口重新拉取。只是这种方式存在延迟,用户可能已经看到恢复失败页面。因此更稳妥的做法仍然是服务端主动同步,同时用 Webhook 作为兜底修正。
五、常见错误和边界条件
延长订阅续订日期接口的限制比较明确:单次调用最多延长 90 天,过期时间超过 60 天的交易无法再延长,且接口只对自动续期订阅有效。如果原交易已经退款、被撤销或者从未有过有效的自动续期订阅,调用会返回 404 或 400。下面整理了常见的错误码和原因。
- 401 Unauthorized:JWT 过期、Issuer ID 错误、API Key 未授权或 Bundle ID 不匹配。
- 404 Not Found:
originalTransactionId不存在,或该交易不属于当前应用。 - 400 BadRequest:
extendByDays超过 90,过期时间已经超过 60 天,或交易类型不是自动续期订阅。 - 429 Too Many Requests:请求频率超过苹果限制,需要稍后重试。
还有一个容易忽视的权限问题是,App Store Connect 中的 API Key 需要具备 App Manager 或相关角色,否则即使 JWT 格式正确,也无法完成延长操作。沙盒测试时要注意生产和沙盒使用不同的域名,同时沙盒交易过期策略可能与生产略有差异,应以真实环境实测为准。
最后需要明确,延长订阅续订日期不等于用户已经重新付费。这个接口更适合客服补偿、重大故障恢复或用户确实遇到支付问题但愿意继续使用服务的场景。滥用延长时间会造成财务结算口径混乱,也容易触发苹果审核关注。每次延长时间、原因码和操作人都应有完整记录,方便对账和审计。
App Store Server API恢复订阅iOS内购修改时间:2026-10-05 05:40:38