如何获取 iOS Subscription 的 paid value 和 currency?
结论
Apple 的 Server-to-Server Notifications 不提供信用卡支付流水中的“实付金额”。
使用 App Store Server Notifications V2 时,可以验证并解码通知中的 signedTransactionInfo,读取以下字段:
price:交易价格,单位为货币的千分之一,即 milliunitscurrency:ISO 4217 货币代码,例如USD、CNY
如果当前通知不含这些字段,可以使用通知里的 transactionId 调用 App Store Server API 的 Get Transaction Info:
GET /inApps/v1/transactions/{transactionId}
接口返回的 signedTransactionInfo 也需要经过 JWS 验证和解码。
通知和 Get Transaction Info 都无法提供 Apple 最终结算给开发者的净收入、税费、佣金或实际到账金额。这些数据需要通过 App Store Connect 的 Sales and Trends 或财务报告查询。
为什么通知正文里看不到金额
App Store Server Notifications V2 的外层正文主要包含通知元数据,交易详情位于:
signedPayload
└── data
└── signedTransactionInfo
signedPayload 和 signedTransactionInfo 都是 JWS 字符串,并非直接展开的 JSON。因此,只查看 webhook 收到的原始正文,通常找不到 price 和 currency。
旧版通知格式可能不含价格字段,对应的交易载荷也可能没有这些信息。在这种情况下,应迁移到 V2,并以 App Store Server API 返回的交易数据为准。
处理步骤
1. 验证并解码外层 signedPayload
不能只做 Base64 解码。生产环境中必须验证 JWS 签名、证书链、Bundle ID 和运行环境,以免接受伪造通知。
验证后可以得到类似结构:
{
"notificationType": "DID_RENEW",
"data": {
"environment": "Production",
"bundleId": "com.example.app",
"signedTransactionInfo": "eyJhbGciOiJFUzI1NiIs..."
}
}
2. 验证并解码 signedTransactionInfo
交易载荷可能包含:
{
"transactionId": "2000000123456789",
"originalTransactionId": "2000000012345678",
"productId": "com.example.premium.monthly",
"purchaseDate": 1724817600000,
"price": 9900,
"currency": "USD"
}
price 的单位是 milliunits,换算公式如下:
displayAmount = price / 1000
例如:
9900 / 1000 = 9.9 USD
不要按照货币通常保留两位小数的方式直接除以 100。Apple 在这里使用固定的千分之一货币单位。
Node.js 示例
可以使用 Apple 官方的 App Store Server Library 完成验签。下面只展示验签后的字段读取逻辑,其中 verifiedTransactionPayload 表示已经通过 JWS 和证书链验证的交易载荷:
function extractTransactionAmount(verifiedTransactionPayload) {
const { transactionId, price, currency } = verifiedTransactionPayload;
if (price == null || !currency) {
return {
transactionId,
amount: null,
currency: null,
reason: "The transaction payload does not contain price or currency."
};
}
return {
transactionId,
priceInMilliunits: price,
amount: price / 1000,
currency
};
}
结果示例:
{
"transactionId": "2000000123456789",
"priceInMilliunits": 9900,
"amount": 9.9,
"currency": "USD"
}
进行财务计算时,尽量不要直接使用浮点数。可以一直将 price 保存为整数形式的 milliunits,仅在最终展示时格式化:
function formatMilliunits(price, currency, locale = "en-US") {
return new Intl.NumberFormat(locale, {
style: "currency",
currency
}).format(price / 1000);
}
通过 Get Transaction Info 补查
如果通知中有 transactionId,但交易载荷缺失或没有保存,可以调用:
GET https://api.storekit.itunes.apple.com/inApps/v1/transactions/{transactionId}
Authorization: Bearer {app_store_server_api_jwt}
Sandbox 环境使用:
GET https://api.storekit-sandbox.itunes.apple.com/inApps/v1/transactions/{transactionId}
响应结构类似:
{
"signedTransactionInfo": "eyJhbGciOiJFUzI1NiIs..."
}
收到响应后,验证并解码 signedTransactionInfo,再读取 price 和 currency。调用接口时使用的 Bearer Token,是通过 App Store Connect In-App Purchase Key 生成的 JWT,不能用用户 receipt 或 transaction JWS 代替。
如果只有 originalTransactionId,可以通过 Get Transaction History 查询该订阅链上的交易,再找到具体的续订交易。金额需要按每个 transactionId 分别保存,因为续订过程中可能出现调价、优惠、升级、降级或货币变化。
注意事项
price和currency表示 App Store 交易价格,不等于开发者净收入。- Apple 的佣金、税务调整、退款和最终结算金额,应通过 App Store Connect 报告核对。
- 不要通过
productId查询商品当前价格,并用它推断历史交易金额。商品价格可能已经调整,交易也可能受到优惠、促销和地区定价影响。 - 同一订阅的不同续订交易可能价格不同,应以具体
transactionId对应的交易载荷为准。 - Webhook 应保存原始通知、通知 UUID、
transactionId、originalTransactionId、整数形式的price和currency,并对重复通知做幂等处理。 - 受交易类型、历史数据或载荷版本影响,
price或currency可能不可用。业务代码应允许字段缺失,不能自行猜测金额。 - 使用金额数据前必须完成 JWS 验签。仅拆分 JWT 并进行 Base64 解码,无法证明数据来自 Apple。
备注:内容仅供参考。
