← 返回藏书阁

Apple Commerce API (ACA)

wiki/ai/concepts/Apple-Commerce-API.md
分类:ai / concepts · 更新:2026-06-14 00:33

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。

获取密钥

  1. App Store Connect → Users and Access → Integrations → In-App Purchase
  2. 创建密钥(需要 Admin 角色)
  3. 下载 .p8 文件(只能下载一次!)
  4. 记录 Key IDIssuer 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 种语言:PythonNode.jsSwiftJava

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_REQUESTApple 请求消费信息(退款争议)
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 /verifyReceiptServer API v2 + Notifications v2
App Receipt (base64)Transaction JWS Token
每次客户端发 Receipt客户端发一次,后续靠 Notification
自行解析 receipt 字段官方库解码 JWS

迁移步骤

  1. 服务端:集成 App Store Server Library
  2. 启用 Notifications v2:App Store Connect → App → App Information → App Store Server Notifications → Version 2
  3. 客户端:SK2 交易后,POST transaction.jsonRepresentation 到你的服务端
  4. 服务端:用 SignedDataVerifier 验证 JWS,不再调 verifyReceipt
  5. 订阅状态:改用 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/

常见陷阱

  1. JWT 过期时间 ≤ 1 小时 — 生成时 exp 不得超过 iat + 3600
  2. p8 密钥只能下载一次 — 丢了就得重新生成,旧的立即失效
  3. Sandbox 和 Production 环境隔离 — 不同 base URL,不同密钥配置
  4. 通知可能乱序 — 用 signedDate 而非到达时间判断先后
  5. REFUND 不代表立即撤销权益 — 根据业务决定是否立即收回
  6. verifyReceipt 将被弃用 — 新项目直接用 Server API v2
  7. CONSUMPTION_REQUEST 12小时超时 — 不回应则 Apple 自动决定退款
  8. App Apple ID 在 Production 必须提供 — Sandbox 可以不传,但 Production 必须传

参考资源