如何通过 Google Android Subscription API 获取订阅到期时间?
结论
如需通过购买令牌查询订阅到期时间,应由可信的服务端调用 Google Play Developer API。新实现建议使用 purchases.subscriptionsv2.get,并从响应的 lineItems[].expiryTime 中读取到期时间。
示例请求:
GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/com.kizbit.pairfinder/purchases/subscriptionsv2/tokens/SUBSCRIPTION_TOKEN
Authorization: Bearer ACCESS_TOKEN
旧版 purchases.subscriptions.get 返回的是 expiryTimeMillis,新项目不建议再依赖这个接口。
题目中的 URL 结构与当年的 v1 文档基本一致。由于响应中没有错误正文,仅凭 503 Service Unavailable 无法判断 access token 是否过期,也不能确定是 URL 拼写或配置出了问题。503 表示 Google API 当时未能处理请求,原因可能是临时服务异常,也可能是早期接口的服务端问题。身份凭证无效通常返回 401,权限或关联配置有误通常返回 403。
推荐调用方式
访问令牌应放在 Authorization 请求头中,不要作为查询参数写进 URL:
curl --fail-with-body \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Accept: application/json" \
"https://androidpublisher.googleapis.com/androidpublisher/v3/applications/com.kizbit.pairfinder/purchases/subscriptionsv2/tokens/${SUBSCRIPTION_TOKEN}"
典型的响应结构如下。实际字段会随订阅状态和订阅模型而变化:
{
"kind": "androidpublisher#subscriptionPurchaseV2",
"subscriptionState": "SUBSCRIPTION_STATE_ACTIVE",
"latestOrderId": "GPA.1234-5678-9012-34567",
"lineItems": [
{
"productId": "subscription99",
"expiryTime": "2026-09-30T12:34:56Z"
}
]
}
expiryTime 使用 RFC 3339 UTC 时间格式。一个订阅可能包含多个 lineItems,不能直接假定数组第一项就是目标项目。应结合 productId、当前时间和业务规则进行选择。例如:
const response = await fetch(
`https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(packageName)}/purchases/subscriptionsv2/tokens/${encodeURIComponent(purchaseToken)}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
const body = await response.json();
if (!response.ok) {
throw new Error(`Google Play API ${response.status}: ${JSON.stringify(body)}`);
}
const matchingItems = (body.lineItems ?? []).filter(
item => item.productId === subscriptionId && item.expiryTime
);
const expiryTimes = matchingItems.map(item => new Date(item.expiryTime));
const expiryTime =
expiryTimes.length > 0
? new Date(Math.max(...expiryTimes.map(date => date.getTime())))
: null;
使用旧接口时
旧接口的调用形式如下:
GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}
Authorization: Bearer ACCESS_TOKEN
对应的命令是:
curl --fail-with-body \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
"https://androidpublisher.googleapis.com/androidpublisher/v3/applications/com.kizbit.pairfinder/purchases/subscriptions/subscription99/tokens/${SUBSCRIPTION_TOKEN}"
成功响应中的 expiryTimeMillis 是 Unix Epoch 毫秒值:
{
"startTimeMillis": "1754000000000",
"expiryTimeMillis": "1756678400000",
"autoRenewing": true
}
转换方式:
const expiryTime = new Date(Number(result.expiryTimeMillis));
expiryTimeMillis 已经以毫秒为单位,不要将它误按秒处理:
// 正确:expiryTimeMillis 已经是毫秒
new Date(Number(result.expiryTimeMillis));
调用前需要确认
SUBSCRIPTION_TOKEN必须是 Google Play Billing 返回的purchaseToken,不能使用订单号、商品 ID、混淆后的账号 ID 或自定义 payload。packageName必须与生成该购买令牌的应用完全一致。- 使用旧接口时,
subscriptionId必须对应此次购买的订阅商品。subscriptionsv2.get不再要求将商品 ID 写入路径。 - 调用方需要具备正确的 OAuth scope,通常为:
https://www.googleapis.com/auth/androidpublisher
- Google Cloud 项目需要启用 Google Play Android Developer API。
- 服务账号或 OAuth 用户必须在相应的 Play Console 开发者账号中获得查看订单和订阅数据所需的权限。
- 接口应由服务端调用,不要将服务账号密钥或可刷新的 OAuth 凭证放入 Android 客户端。
为什么示例请求返回 503
这个响应表明请求已经到达 Google API,但服务端当时未能完成处理。它不表示授权令牌已经过期:
401 Unauthorized:令牌缺失、无效或已过期。403 Forbidden:API 未启用、账号未授权、Play Console 权限不足或配额受限等。400或404:参数、应用、商品或购买令牌不匹配,具体状态取决于接口。429:请求频率过高或受到配额限制。500、502、503、504:通常应按临时服务端故障处理。
示例使用的是 2012 年的 v1 接口,响应中又没有 JSON 错误正文,因此现有信息不足以判断 Google 内部失败的具体原因。处理时应记录响应头和请求时间,并采用指数退避重试,例如:
const retryableStatus = new Set([429, 500, 502, 503, 504]);
const delays = [1000, 2000, 4000, 8000, 16000];
for (let attempt = 0; attempt <= delays.length; attempt++) {
const response = await callGooglePlay();
if (response.ok) {
return await response.json();
}
const errorBody = await response.text();
if (!retryableStatus.has(response.status) || attempt === delays.length) {
throw new Error(`Google Play API ${response.status}: ${errorBody}`);
}
const jitter = Math.floor(Math.random() * 500);
await new Promise(resolve => setTimeout(resolve, delays[attempt] + jitter));
}
如果请求持续返回 503,可以先改用当前的 v3 subscriptionsv2 地址重新测试,然后检查 Google Cloud API 是否已启用、Play Console 权限是否正确,以及令牌与包名是否匹配。如果这些项目都没有问题,且故障能够稳定复现,再将请求时间、HTTP 状态、响应正文和 Google 返回的请求标识一并提供给 Google Play 开发者支持。
示例中还公开了 OAuth access token。即使该令牌很可能早已失效,只要有效凭证曾出现在日志、工单或公开页面中,都应立即撤销并重新签发。
备注:内容仅供参考。
