Apple Commerce API (ACA)
Apple Commerce API (ACA)
Apple 服务端支付管理套件:App Store Server API v2 + Server Notifications v2 + Advanced Commerce API。
与客户端 StoreKit 2 配合,实现完整的支付验证、订阅管理、退款处理。
ACA = Apple Commerce APIs 的简称,涵盖服务端所有 IAP 相关接口。
架构总览
┌──────────┐ StoreKit 2 ┌──────────┐
│ iOS App │ ──── purchase ────→ │ Apple │
│ │ ←── JWS token ──── │ Store │
└────┬─────┘ └─────┬────┘
│ POST /verify │
↓ │ Server Notifications v2
┌──────────┐ ↓
│ Your │ ←──── webhook ──────── POST /notifications
│ Server │ ↑
│ │ ─── API calls ────→ App Store Server API v2
└──────────┘ (JWT auth)
认证:JWT (ES256)
所有 Server API 调用使用 JWT 认证,签名算法 ES256。
获取密钥
- App Store Connect → Users and Access → Integrations → In-App Purchase
- 创建密钥(需要 Admin 角色)
- 下载 .p8 文件(只能下载一次!)
- 记录 Key ID 和 Issuer ID
生成 JWT
import jwt
import time
import uuid
def generate_token(key_id, issuer_id, private_key_path):
with open(private_key_path, 'r') as f:
private_key = f.read()
now = int(time.time())
payload = {
"iss": issuer_id, # issuer ID from ASC
"iat": now, # issued at
"exp": now + 3600, # expiry (max 1 hour)
"aud": "appstoreconnect-v1",
"bid": "com.example.app", # bundle ID
}
headers = {
"alg": "ES256",
"kid": key_id,
"typ": "JWT"
}
return jwt.encode(payload, private_key, algorithm="ES256", headers=headers)
使用官方库(推荐)
# pip install app-store-server-library
from appstoreserverlibrary.api_client import AppStoreServerAPIClient
from appstoreserverlibrary.models.Environment import Environment
private_key = open("SubscriptionKey_ABCDEFGHIJ.p8").read()
client = AppStoreServerAPIClient(
signing_key=private_key,
key_id="ABCDEFGHIJ",
issuer_id="99b16628-15e4-6668-972b-eeff55eeff55",
bundle_id="com.example.app",
environment=Environment.SANDBOX # or Environment.PRODUCTION
)
官方库提供 4 种语言:Python、Node.js、Swift、Java。
App Store Server API v2
核心 API 端点
| 端点 | 用途 |
GET /inApps/v1/transactions/{originalTransactionId} | 获取交易历史 |
GET /inApps/v1/subscriptions/{originalTransactionId} | 获取订阅状态 |
GET /inApps/v1/refund/lookup/{originalTransactionId} | 查询退款历史 |
GET /inApps/v1/notifications | 获取通知历史 |
POST /inApps/v1/notifications/test | 发送测试通知 |
GET /inApps/v2/history/{transactionId} | 获取交易历史(V2,含更多信息) |
获取交易历史
from appstoreserverlibrary.receipt_utility import ReceiptUtility
from appstoreserverlibrary.models.TransactionHistoryRequest import TransactionHistoryRequest, ProductType, Order
receipt_util = ReceiptUtility()
app_receipt = "MI..." # 从客户端获取的 App Receipt
transaction_id = receipt_util.extract_transaction_id_from_app_receipt(app_receipt)
request = TransactionHistoryRequest(
sort=Order.ASCENDING,
revoked=False,
productTypes=[ProductType.AUTO_RENEWABLE]
)
# 分页获取全部历史
response = client.get_transaction_history(transaction_id, None, request, GetTransactionHistoryVersion.V2)
while response.has_more:
response = client.get_transaction_history(transaction_id, response.revision, request, GetTransactionHistoryVersion.V2)
获取订阅状态
status_response = client.get_all_subscription_statuses(original_transaction_id)
for group in status_response:
for info in group.last_transactions:
print(f"Status: {info.status}") # ACTIVE, EXPIRED, etc.
# info.signed_transaction_info 是 JWS 编码的交易信息
Server Notifications v2
机制
Apple 向你的服务端 POST signed payload,包含交易状态变更通知。
关键通知类型
| 类型 | 含义 |
DID_RENEW | 订阅续期成功 |
DID_FAIL_TO_RENEW | 续期失败(正在重试) |
EXPIRED | 订阅过期 |
DID_CHANGE_RENEWAL_STATUS | 用户关闭/开启自动续期 |
REFUND | 退款 |
REVOKE | 撤销(家庭共享取消 / Apple 撤销) |
CONSUMPTION_REQUEST | Apple 请求消费信息(退款争议) |
ONE_TIME_CHARGE | 一次性购买 |
RENEWAL_EXTENDED | 续期延长 |
验证通知签名
from appstoreserverlibrary.signed_data_verifier import SignedDataVerifier
verifier = SignedDataVerifier(
root_certificates=root_cas, # Apple Root CA 证书
enable_online_checks=True,
environment=Environment.SANDBOX,
bundle_id="com.example.app",
app_apple_id=None # Production 必须提供
)
# notificationPayload 是 POST body 的 signedPayload 字段
payload = verifier.verify_and_decode_notification(notification_payload)
print(payload.notificationType) # "DID_RENEW", "REFUND" 等
通知 payload 结构
{
"notificationType": "DID_RENEW",
"subtype": "UPGRADE",
"data": {
"appAppleId": 1234567890,
"bundleId": "com.example.app",
"signedTransactionInfo": "eyJ...", // JWS, 解码后为 TransactionInfo
"signedRenewalInfo": "eyJ...", // JWS, 解码后为 RenewalInfo
"environment": "Production"
}
}
处理 CONSUMPTION_REQUEST
当用户请求退款时,Apple 会发送 CONSUMPTION_REQUEST,你需要在 12 小时内回应:
# 调用 send_consumption_data 回应
from appstoreserverlibrary.models.ConsumptionRequest import ConsumptionRequest, ConsumptionStatus
request = ConsumptionRequest(
customer_consented_status=True,
consumption_status=ConsumptionStatus.UNCONSUMED,
delivery_status=DeliveryStatus.DELIVERED,
# ... 更多字段
)
client.send_consumption_data(original_transaction_id, request)
从 verifyReceipt 迁移
| 旧 API | 新 API |
POST /verifyReceipt | Server API v2 + Notifications v2 |
| App Receipt (base64) | Transaction JWS Token |
| 每次客户端发 Receipt | 客户端发一次,后续靠 Notification |
| 自行解析 receipt 字段 | 官方库解码 JWS |
迁移步骤
- 服务端:集成 App Store Server Library
- 启用 Notifications v2:App Store Connect → App → App Information → App Store Server Notifications → Version 2
- 客户端:SK2 交易后,POST
transaction.jsonRepresentation到你的服务端 - 服务端:用
SignedDataVerifier验证 JWS,不再调 verifyReceipt - 订阅状态:改用
get_all_subscription_statuses而非解析 receipt
Advanced Commerce API
iOS 18 / macOS 15 引入的新 API,用于更复杂的商业模式:
- 自定义订阅时长(非固定月/年)
- 递增定价(按用量计费)
- 按地区定价策略
- 需要 App Store Connect 额外配置
当前仅适用于特定场景,大多数 App 不需要。
最佳实践
1. 双重验证
# 客户端 SK2 已验证 → 服务端再验证一次
# 不要信任任何客户端数据
verified = verifier.verify_and_decode_notification(jws_token)
2. 幂等处理通知
# 同一通知可能发送多次,用 originalTransactionId + notificationType 做幂等
def handle_notification(payload):
key = f"{payload.data.originalTransactionId}:{payload.notificationType}"
if redis.exists(key):
return # 已处理
# 处理逻辑...
redis.set(key, "1", ex=86400)
3. 订阅状态以服务端为准
# 不要依赖客户端传来的状态
# 客户端可能离线、缓存过期、被篡改
# 收到 Notification 后立即更新数据库
4. 处理所有通知类型
# 不要只处理 DID_RENEW 和 REFUND
# CONSUMPTION_REQUEST、REVOKE、DID_FAIL_TO_RENEW 都很重要
# 遗漏会导致权益不一致
5. 重试机制
# Apple 通知不是 exactly-once
# 最多重试 ~10 次,间隔递增
# 你的 webhook 端点必须返回 200,否则 Apple 会重试
6. 安全存储 Apple Root CA
# 不要每次从 Apple 网站下载 Root CA
# 打包到你的服务中,定期更新
# Apple Root CA — G3 (当前有效)
# https://www.apple.com/certificateauthority/
常见陷阱
- JWT 过期时间 ≤ 1 小时 — 生成时
exp不得超过iat + 3600 - p8 密钥只能下载一次 — 丢了就得重新生成,旧的立即失效
- Sandbox 和 Production 环境隔离 — 不同 base URL,不同密钥配置
- 通知可能乱序 — 用
signedDate而非到达时间判断先后 - REFUND 不代表立即撤销权益 — 根据业务决定是否立即收回
- verifyReceipt 将被弃用 — 新项目直接用 Server API v2
- CONSUMPTION_REQUEST 12小时超时 — 不回应则 Apple 自动决定退款
- App Apple ID 在 Production 必须提供 — Sandbox 可以不传,但 Production 必须传