지난 글에서는 애플 인앱 결제 상품 중 개별 상품 API에 대해서 알아봤습니다. 개별 상품의 생성, 조회, 수정, 삭제를 구현하고 약 1일만에 심사까지 통과 했습니다. 이번 글에서는 두 번째 상품 종류인 구독 상품(Auto-Renewable Subscriptions)에 대해 알아보도록 하겠습니다. API Key, JWT 등 기본 설정 방법은 인앱 결제 API를 참고해주세요.
기본 흐름
구독 상품의 경우도 개별 상품과 마찬가지로 생성하는데 있어 많은 단계를 진행해야 합니다. 흐름은 다음과 같습니다.
1. 구독 그룹 생성: 구독 그룹을 생성하고 이름을 설정합니다. 생성 이후 동일한 구독 그룹 내에 상품을 만들 경우 구독 그룹 ID만 재사용하고, 추가적인 호출은 필요 없습니다.
2. 구독 그룹 현지화 정보 생성: 구독 그룹의 현지화 정보를 생성합니다. 1번과 마찬가지로 동일한 구독 그룹을 사용하는 경우 1회만 호출하고 이후 추가적인 호출은 필요 없습니다.
3. 구독 그룹 내 구독 상품 생성: 구독 그룹 내 구독 상품을 생성합니다. 상품명(name), 상품 ID(productID), 상품 설명(reviewNote), 결제 주기(subscriptionPeriod)를 설정합니다.
4. 구독 상품 지역 설정: 상품을 구매할 수 있는 지역을 설정합니다.
5. 구독 상품 가격 설정: 상품의 가격을 설정합니다. 여기서는 1) 사용 가능한 가격 및 가격 ID, 2) 상품 가격 설정, 3) 가격에 해당하는 타 국가 가격 ID 조회, 4) 타 국가 가격 설정으로 구성됩니다.
6. 구독 상품 현지화 정보 설정: 각각의 지역에서 사용자에게 노출을 다르게 설정할 수 있습니다.
7. 구독 상품 스크린샷 등록: 상품 심사를 위한 앱 스크린샷을 제출합니다. 여기서는 1) 스크린샷 영역 설정, 2) 스크린샷 업로드, 3) 스크린샷 커밋, 4) 스크린샷 업로드 상태 확인으로 구성됩니다.
8. 구독 그룹 심사 등록: 구독 그룹 심사를 제출합니다. 1, 2번과 마찬가지로 한 번 심사 승인 이후 재호출은 불필요합니다.
9. 구독 상품 심사 등록: 구독 그룹 내 구독 상품 심사를 제출합니다.
구독 그룹 생성
구독 그룹을 생성하는 단계입니다. 구독 그룹 이름만 설정하면 되며, 응답값으로 구독 그룹 ID를 받을 수 있습니다.
API Endpoint
POST /v1/subscriptionGroups
요청 예시:
async def create_apple_subs_group(client: httpx.AsyncClient) -> int:
"""
Apple 구독 그룹 생성
1회만 호출하여 subscription_group_id 저장 후, 동일 ID로 호출
"""
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptionGroups"
payload = {
"data": {
"type": "subscriptionGroups",
"attributes": {
"referenceName": name
},
"relationships": {
"app": {
"data": {
"type": "apps",
"id": APPLE_STORE_APP_ID
}
}
}
}
}
response = await apple_service.call_apple_api(client, "POST", url, json=payload)
data = response.json()
return data["data"]["id"]
응답 예시:
{
"data": {
"type": "subscriptionGroups",
"id": "21892785",
"attributes": {
"referenceName": name
},
"relationships": {
"subscriptions": {
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionGroups/21892785/relationships/subscriptions",
"related": "https://api.appstoreconnect.apple.com/v1/subscriptionGroups/21892785/subscriptions"
}
},
"subscriptionGroupLocalizations": {
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionGroups/21892785/relationships/subscriptionGroupLocalizations",
"related": "https://api.appstoreconnect.apple.com/v1/subscriptionGroups/21892785/subscriptionGroupLocalizations"
}
}
}
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionGroups"
}
}
여기서 응답받는 id가 구독 그룹의 ID입니다. 글에서는 subscription_group_id로 사용하고 있습니다.
적용 영역:

구독 그룹 현지화 정보 생성
구독 그룹이 지역에 따라 사용자에게 노출 되는 정보를 다르게 설정할 수 있습니다.
API Endpoint
POST /v1/subscriptionGroupLocalizations
요청 예시:
async def create_apple_subs_group_localization(
client: httpx.AsyncClient,
subscription_group_id: int
) -> None:
"""
Apple 구독 그룹 현지화 정보 설정
1회만 호출하여 등록 해두면 추후 호출할 필요 없음
"""
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptionGroupLocalizations"
payload = {
"data": {
"type": "subscriptionGroupLocalizations",
"attributes": {
"name": "그낙이네 구독 상품 그룹",
"locale": "ko",
},
"relationships": {
"subscriptionGroup": {
"data": {
"type": "subscriptionGroups",
"id": subscription_group_id
}
}
}
}
}
response = await apple_service.call_apple_api(client, "POST", url, json=payload)
response.raise_for_status()
return
응답 예시:
{
"data": {
"type": "subscriptionGroupLocalizations",
"id": "512e14fb-3934-4eed-a11f-78fd75bd62ff",
"attributes": {
"name": localized_name,
"customAppName": null,
"locale": "ko",
"state": "PREPARE_FOR_SUBMISSION"
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionGroupLocalizations/512e14fb-3934-4eed-a11f-78fd75bd62ff"
}
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionGroupLocalizations"
}
}
적용 예시:

구독 상품 생성
구독 그룹 내 구독 상품을 생성합니다. 기본적으로 상품명, 상품 ID, 상품 설명, 결제 주기를 설정합니다. 상품명과 상품 ID는 앱 전체에서 중복될 수 없는 유니크 값입니다. 개별 상품 생성과 마찬가지로 큰 틀을 만드는 단계입니다.
API Endpoint
POST /v1/subscriptions
요청 예시:
async def create_apple_subs_metadata(
client: httpx.AsyncClient,
subscription_group_id: int,
product_id: str,
product_name: str,
description: str,
monthly: int
) -> int:
"""
Apple Subscriptions 메타데이터 생성
"""
valid_months = {1, 3, 6, 12}
if monthly not in valid_months:
raise ValueError(f"결제 주기는 {valid_months}개월 중 하나여야 합니다.")
period_map = {
1: "ONE_MONTH",
3: "THREE_MONTHS",
6: "SIX_MONTHS",
12: "ONE_YEAR"
}
period = period_map[monthly]
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptions"
payload = {
"data": {
"type": "subscriptions",
"attributes": {
"name": product_name,
"productId": product_id,
"subscriptionPeriod": period, # ONE_MONTH, TWO_MONTHS, THREE_MONTHS, SIX_MONTHS, ONE_YEAR
"reviewNote": description,
"groupLevel": 1,
},
"relationships": {
"group": {
"data": {
"type": "subscriptionGroups",
"id": subscription_group_id
}
}
}
}
}
response = await apple_service.call_apple_api(client, "POST", url, json=payload)
data = response.json()
return data["data"]["id"]
응답 예시:
{
"data": {
"type": "subscriptions",
"id": "6757855490",
"attributes": {
"name": "GNAAK FOR 1 MONTH",
"productId": "SUBS.GNAAK.1000P",
"familySharable": false,
"state": "MISSING_METADATA",
"subscriptionPeriod": "ONE_MONTH",
"reviewNote": "THIS IS ALL ACCESS FOR GNAAK TIER 1",
"groupLevel": 1
},
/* ... */
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptions"
}
}
여기서 응답 받은 id 값을 subscription_id로 사용했습니다.
적용 영역:

구독 상품 지역 설정
상품이 사용 가능한 지역을 설정해줍니다.
API Endpoint
POST /v1/subscriptionAvailabilities
요청 예시:
async def create_apple_subs_availability(
client: httpx.AsyncClient,
subscription_id: int
) -> None:
"""
Apple Subscriptions 사용 가능 지역 설정
"""
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptionAvailabilities"
payload = {
"data": {
"type": "subscriptionAvailabilities",
"attributes": {
"availableInNewTerritories": False,
},
"relationships": {
"availableTerritories": {
"data": [{
"type": "territories",
"id": "KOR",
}]
},
"subscription": {
"data": {
"type": "subscriptions",
"id": subscription_id,
}
}
}
}
}
response = await apple_service.call_apple_api(client, "POST", url, json=payload)
response.raise_for_status()
return
응답 예시:
{
"data": {
"type": "subscriptionAvailabilities",
"id": "6757855490",
"attributes": {
"availableInNewTerritories": false
},
"relationships": {
"availableTerritories": {
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionAvailabilities/6757855490/relationships/availableTerritories",
"related": "https://api.appstoreconnect.apple.com/v1/subscriptionAvailabilities/6757855490/availableTerritories"
}
}
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionAvailabilities/6757855490"
}
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionAvailabilities"
}
}
적용 영역:

구독 상품 가격 설정
사용 가능 가격 조회
개별 상품 생성과 마찬가지로 애플에서는 가격에 해당하는 가격 ID를 등록해야합니다.
API Endpoint
필터와 제한 개수를 통해 한국 가격 ID를 찾을 수 있습니다. 가격대마다 limit 내에 존재 여부가 달라지므로 확인하고 사용하시길 바랍니다. 구독 상품은 개별 상품과 다르게 v1을 사용합니다
GET /v1/subscriptions/{subscription_id}/pricePoints?filter[territory]=KOR&include=territory&limit=100
요청 예시:
async def get_apple_subs_price_point_id(
client: httpx.AsyncClient,
subscription_id: int,
price: int) -> Optional[str]:
"""
Apple Subscriptions 사용 가능 가격 조회
"""
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptions/{subscription_id}/pricePoints?filter[territory]=KOR&include=territory&limit=100"
response = await apple_service.call_apple_api(client, "GET", url)
data=response.json()
price_points = data["data"]
target_price = price
price_point_id = None
for price_point in price_points:
if price_point["attributes"]["customerPrice"] == str(target_price):
price_point_id = price_point["id"]
break
if not price_point_id:
raise ValueError("선택하신 가격은 애플에서 지원하지 않습니다.")
return price_point_id
응답 예시:
{
"data": [
{
"type": "subscriptionPricePoints",
"id": "eyJzIjoiNjc1Nzg1NTQ5MCIsInQiOiJLT1IiLCJwIjoiMTAwMDEifQ",
"attributes": {
"customerPrice": "400",
"proceeds": "291",
"proceedsYear2": "345"
},
"relationships": {
"territory": {
"data": {
"type": "territories",
"id": "KOR"
}
},
"equalizations": {
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionPricePoints/eyJzIjoiNjc1Nzg1NTQ5MCIsInQiOiJLT1IiLCJwIjoiMTAwMDEifQ/relationships/equalizations",
"related": "https://api.appstoreconnect.apple.com/v1/subscriptionPricePoints/eyJzIjoiNjc1Nzg1NTQ5MCIsInQiOiJLT1IiLCJwIjoiMTAwMDEifQ/equalizations"
}
}
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionPricePoints/eyJzIjoiNjc1Nzg1NTQ5MCIsInQiOiJLT1IiLCJwIjoiMTAwMDEifQ"
}
},
/* … 중간 price point 동일 구조 … */
],
"included": [
{
"type": "territories",
"id": "KOR",
"attributes": {
"currency": "KRW"
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/territories/KOR"
}
}
],
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptions/6757855490/pricePoints?filter%5Bterritory%5D=KOR&include=territory&limit=10",
"next": "https://api.appstoreconnect.apple.com/v1/subscriptions/6757855490/pricePoints?filter%5Bterritory%5D=KOR&include=territory&cursor=Cg&limit=10"
},
"meta": {
"paging": {
"total": 800,
"nextCursor": "Cg",
"limit": 10
}
}
}
구독 상품 가격 설정
이제 받아온 price_point_id와 구독 상품의 id인 subscription_id를 사용해서 상품 가격을 설정해줍시다.
API Endpoint
POST /v1/subscriptionPrices
요청 예시:
async def set_apple_subs_price(
client: httpx.AsyncClient,
subscription_id: int,
price_point_id: str
) -> None:
"""
Apple Subscriptions 상품 가격 설정
"""
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptionPrices"
payload = {
"data": {
"type": "subscriptionPrices",
"relationships": {
"subscription": {
"data": {
"type": "subscriptions",
"id": subscription_id,
}
},
"subscriptionPricePoint": {
"data": {
"type": "subscriptionPricePoints",
"id": price_point_id
}
}
},
"attributes": {
"preserveCurrentPrice": True
}
},
}
response = await apple_service.call_apple_api(client, "POST", url, json=payload)
response.raise_for_status()
return
응답 예시:
{
"data": {
"type": "subscriptionPrices",
"id": "eyJhIjoiNjc1Nzg1NTQ5MCIsImMiOiJLUiIsImQiOjAsInAiOiIwIn0",
"attributes": {
"startDate": null,
"preserved": false
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionPrices/eyJhIjoiNjc1Nzg1NTQ5MCIsImMiOiJLUiIsImQiOjAsInAiOiIwIn0"
}
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionPrices"
}
}
적용 영역:

가격에 해당하는 타 국가 가격 조회
구독 상품 가격 설정에서는 한국(사용 국가) 가격만 등록하는게 아니라, 모든 국가 가격을 다 등록해줘야합니다. (공식 문서에는 "If the subscription is available in all territories, an individual POST /v1/subscriptionPrices call is necessary for each territory. You might consider automating this step."라고 나와있는데, 가격 등록을 다 안해주면 심사 요청이 막히더라구요.)
API Endpoint
총 175개국을 등록해줘야 하기 때문에 limit은 200으로 고정해서 사용했습니다.
GET /v1/subscriptionPricePoints/{price_point_id}/equalizations?include=territory&limit=200
요청 예시:
async def get_apple_subs_price_point_id_all(
client: httpx.AsyncClient,
price_point_id: str
) -> List[Dict[str,str]]:
"""
Apple Subscriptions 다른 국가 상품 ID 조회
"""
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptionPricePoints/{price_point_id}/equalizations?include=territory&limit=200"
response = await apple_service.call_apple_api(client, "GET", url)
data = response.json()
result = []
country_list = data["data"]
for country in country_list:
result.append({
"price_point_id": country["id"],
"country_code": country["relationships"]["territory"]["data"]["id"]
})
return result
응답 예시:
{
"data": [
{
"type": "subscriptionPricePoints",
"id": "eyJzIjoiNjc1NzkxMDYxMSIsInQiOiJBRkciLCJwIjoiMTAwNDkifQ",
"attributes": {
"customerPrice": "3.99",
"proceeds": "2.8",
"proceedsYear2": "3.39"
},
"relationships": {
"territory": {
"data": {
"type": "territories",
"id": "AFG"
}
},
"equalizations": {
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionPricePoints/eyJzIjoiNjc1NzkxMDYxMSIsInQiOiJBRkciLCJwIjoiMTAwNDkifQ/relationships/equalizations",
"related": "https://api.appstoreconnect.apple.com/v1/subscriptionPricePoints/eyJzIjoiNjc1NzkxMDYxMSIsInQiOiJBRkciLCJwIjoiMTAwNDkifQ/equalizations"
}
}
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionPricePoints/eyJzIjoiNjc1NzkxMDYxMSIsInQiOiJBRkciLCJwIjoiMTAwNDkifQ"
}
}
/* 동일한 구조의 subscriptionPricePoints가 계속 반복 (총 174개) */
],
"included": [
{
"type": "territories",
"id": "AFG",
"attributes": {
"currency": "USD"
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/territories/AFG"
}
},
{
"type": "territories",
"id": "AUS",
"attributes": {
"currency": "AUD"
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/territories/AUS"
}
}
/* territory 메타데이터 반복 */
],
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionPricePoints/{KOR_PRICE_POINT_ID}/equalizations?include=territory",
"next": "https://api.appstoreconnect.apple.com/v1/subscriptionPricePoints/{KOR_PRICE_POINT_ID}/equalizations?include=territory&cursor=Mg"
},
"meta": {
"paging": {
"total": 174,
"limit": 50,
"nextCursor": "Mg"
}
}
}
타 국가 가격 설정
이제 받아온 응답 데이터로 타 국가에도 동일하게 가격을 설정해줍시다. 위에서 return한 result를 바로 others로 사용했습니다. 다른 국가 가격 설정도 위와 동일한 함수를 호출하면 됩니다. 또한 개별 국가 등록은 병렬로 처리해도 문제가 없기 때문에 10개씩 묶어서 API를 호출했습니다.
API Endpoint
POST /v1/subscriptionPrices
요청 예시:
async def set_apple_subs_price_all(
client: httpx.AsyncClient,
subscription_id: int,
others: list
) -> None:
"""
Apple Subscriptions 다른 국가 가격 설정
"""
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptionPrices"
semaphore = asyncio.Semaphore(10)
async def post_one(other: dict):
async with semaphore:
payload = {
"data": {
"type": "subscriptionPrices",
"relationships": {
"subscription": {
"data": {
"type": "subscriptions",
"id": subscription_id,
}
},
"subscriptionPricePoint": {
"data": {
"type": "subscriptionPricePoints",
"id": other["price_point_id"]
}
}
},
},
}
response = await apple_service.call_apple_api(client, "POST", url, json=payload)
response.raise_for_status()
print(f"{other['country_code']} 성공!")
tasks = [post_one(other) for other in others]
await asyncio.gather(*tasks, return_exceptions=True)
구독 상품 현지화 정보 설정
상품이 지역에 따라 사용자에게 노출 되는 정보를 다르게 설정할 수 있습니다. 위에서 사용 가능 지역을 한국만 선택했으니, 상품 현지화 정보도 한국어로만 설정해줍시다.
API Endpoint
POST /v1/subscriptionLocalizations
요청 예시:
async def create_apple_subs_localization(
client: httpx.AsyncClient,
subscription_id: int,
product_name: str,
description: str
) -> None:
"""
Apple Subscriptions 현지화 정보 설정
"""
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptionLocalizations"
payload = {
"data": {
"type": "subscriptionLocalizations",
"attributes": {
"locale": "ko",
"name": product_name,
"description": description
},
"relationships": {
"subscription": {
"data": {
"type": "subscriptions",
"id": subscription_id
}
}
}
}
}
response = await apple_service.call_apple_api(client, "POST", url, json=payload)
response.raise_for_status()
return
응답 예시:
{
"data": {
"type": "subscriptionLocalizations",
"id": "aab2977d-43b4-4632-8227-c1cf87e92a98",
"attributes": {
"name": "IOS 구독 상품",
"locale": "ko",
"description": "이것은 애플스토어 구독 상품 생성 API로 생성된 구독 상품입니다.",
"state": "PREPARE_FOR_SUBMISSION"
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionLocalizations/aab2977d-43b4-4632-8227-c1cf87e92a98"
}
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionLocalizations"
}
}
적용 영역:

스크린샷 등록하기
스크린샷 정보 등록
API Endpoint
POST /v1/subscriptionAppStoreReviewScreenshots
요청 예시:
async def create_apple_subs_screenshot_metadata(
client: httpx.AsyncClient,
subscription_id: int
) -> Dict[str, Any]:
"""
Apple Subscriptions 스크린샷 메타 데이터 생성
"""
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptionAppStoreReviewScreenshots"
# TODO: 실제 업로드 하는 스크린샷으로 나중에 바꿔야 할수도
file_path = MEDIA_ROOT / "sub_screenshot.png"
file_name = file_path.name
file_size = file_path.stat().st_size
payload = {
"data": {
"type": "subscriptionAppStoreReviewScreenshots",
"attributes": {
"fileName": file_name,
"fileSize": file_size
},
"relationships": {
"subscription": {
"data": {
"type": "subscriptions",
"id": subscription_id,
}
},
}
}
}
response = await apple_service.call_apple_api(client, "POST", url, json=payload)
data = response.json()
screenshot_id = data["data"]["id"]
upload_operation = data["data"]["attributes"]["uploadOperations"]
return {"id": screenshot_id, "method": upload_operation}
응답 예시:
{
"data": {
"type": "subscriptionAppStoreReviewScreenshots",
"id": "64c9c7f5-4542-462c-9aea-ddfbc10fe6c2",
"attributes": {
"fileSize": 423676,
"fileName": "screenshot.png",
"sourceFileChecksum": "",
"imageAsset": {
"templateUrl": "",
"width": 0,
"height": 0
},
"assetToken": "PurpleSource221/v4/e8/2f/91/e82f917f-0244-68e2-e336-5cae95f32a90/screenshot.png",
"assetType": "SCREENSHOT",
"uploadOperations": [
{
"method": "PUT",
"url": "https://northamerica-1.object-storage.apple.com/itmspod12-assets-massilia-200001/PurpleSource221/v4/e8/2f/91/e82f917f-0244-68e2-e336-5cae95f32a90/SHbdIQBRD00atOapEbQ_LNSGivG4x8cL6lIWbN5Zt_Q_U003d-1768480941878?partNumber=1&uploadId=a2d56290-f20f-11f0-b0b8-70b2b91d9cf1&apple-asset-repo-correlation-key=DADDG2HTEI4KRASLE5X62HSDCE&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260115T124222Z&X-Amz-SignedHeaders=host&X-Amz-Credential=MKIAP7F9QNTEY48OTE7F%2F20260115%2Fnorthamerica-1%2Fs3%2Faws4_request&X-Amz-Expires=604800&X-Amz-Signature=7abe854a993db80edb9b4b9644b1ca14e6733cc4a2be0c43f55a453016443f63",
"length": 423676,
"offset": 0,
"requestHeaders": [
{
"name": "Content-Type",
"value": "image/png"
}
]
}
],
"assetDeliveryState": {
"errors": null,
"warnings": null,
"state": "AWAITING_UPLOAD"
}
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionAppStoreReviewScreenshots/64c9c7f5-4542-462c-9aea-ddfbc10fe6c2"
}
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/subscriptionAppStoreReviewScreenshots"
}
}
여기서 받아오는 id와 uploadOperations를 리턴해줍시다. 각각 screenshot_id와 upload_ops로 사용했습니다.
스크린샷 업로드
이제 스크린샷을 등록해줍시다. upload_ops가 리스트 형태로 넘어오는데 단일 사진만 제출해도 충분하기 때문에 upload_op = upload_ops[0]를 변수로 넘겨줍시다.
async def upload_apple_subs_screenshot(
client: httpx.AsyncClient,
upload_op: dict
) -> None:
"""
Apple Subscriptions 스크린샷 업로드
"""
url = upload_op["url"]
request_header = upload_op["requestHeaders"][0]
headers = {
request_header["name"]: request_header["value"]
}
file_path = MEDIA_ROOT / "sub_screenshot.png"
with file_path.open("rb") as f:
content=f.read()
if not content:
raise ValueError("파일을 읽을 수 없습니다.")
response = await client.put(url, data=content, headers=headers)
response.raise_for_status()
return
스크린샷 커밋
개별 상품과 마찬가지로 커밋을 진행해줍시다.
API Endpoint
PATCH /v1/subscriptionAppStoreReviewScreenshots/{screenshot_id}
요청 예시:
async def commit_apple_subs_screenshot(
client: httpx.AsyncClient,
screenshot_id: str
) -> None:
"""
Apple Subscription 업로드 스크린샷 커밋
"""
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptionAppStoreReviewScreenshots/{screenshot_id}"
payload = {
"data": {
"type": "subscriptionAppStoreReviewScreenshots",
"id": screenshot_id,
"attributes": {
"uploaded": True
},
}
}
response = await apple_service.call_apple_api(client, "PATCH", url, json=payload)
response.raise_for_status()
return
스크린샷 상태 확인
이제 커밋된 스크린샷이 업로드 완료될 때까지 상태 확인을 통해서 심사 신청을 너무 일찍 넣는 실수를 방지해봅시다.
API Endpoint
GET /v1/subscriptionAppStoreReviewScreenshots/{screenshot_id}
요청 예시:
async def check_apple_subs_screenshot(
client: httpx.AsyncClient,
screenshot_id: str
) -> None:
"""
Apple Subscription 업로드 스크린샷 상태 확인
"""
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptionAppStoreReviewScreenshots/{screenshot_id}"
start = time.time()
interval = 5
timeout = 60
while True:
response = await apple_service.call_apple_api(client, "GET", url)
response.raise_for_status()
data = response.json()
status = data["data"]["attributes"]["assetDeliveryState"]["state"]
if status == "COMPLETE":
return
if time.time() - start > timeout:
raise TimeoutError("기다리는중...")
await asyncio.sleep(interval)
심사 등록하기
이제 심사 등록을 하기 위한 모든 메타데이터 설정을 완료했습니다. 이제 구독 그룹과 그룹 내 구독 상품에 대해서 심사 제출을 진행해봅시다. 위에서 언급한 바와 같이, 구독 그룹의 경우 1회만 심사 승인이 되면 추후 재호출은 불필요합니다.
구독 그룹 심사 등록하기
API Endpoint
POST /v1/subscriptionGroupSubmissions
요청 예시:
async def submit_apple_subs_group_review(
client: httpx.AsyncClient,
subscription_group_id: int
) -> None:
"""
Apple In-App Purchase 심사 등록
"""
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptionGroupSubmissions"
payload = {
"data": {
"type": "subscriptionGroupSubmissions",
"relationships": {
"subscriptionGroup": {
"data": {
"id": subscription_group_id,
"type": "subscriptionGroups"
}
}
}
}
}
response = await apple_service.call_apple_api(client, "POST", url, json=payload)
response.raise_for_status()
return
구독 상품 심사 등록하기
API Endpoint
POST /v1/subscriptionSubmissions
요청 예시:
async def submit_apple_subs_review(
client: httpx.AsyncClient,
subscription_id: int
) -> None:
url = f"{APPLE_STORE_BASE_URL}/v1/subscriptionSubmissions"
payload = {
"data": {
"type": "subscriptionSubmissions",
"relationships": {
"subscription": {
"data": {
"id": subscription_id,
"type": "subscriptions"
}
}
}
}
}
response = await apple_service.call_apple_api(client, "POST", url, json=payload)
response.raise_for_status()
return
이번 글에서는 애플 인앱 상품 중 구독 상품(Auto-Renewable Subscriptions)의 생성 과정에 대해서 알아봤습니다. 개별 상품과 마찬가지로 호출해야 하는 API 개수가 굉장히 많네요. 대략적으로 1~3일 이내에 심사가 끝이 나며 스케쥴러를 12시간 간격으로 돌려 DB와 애플 서버를 동기화 시켜줄 예정입니다. 다음 글에서는 구독 상품 조회와 수정, 그리고 삭제에 대해서 알아보도록 하겠습니다.
'기능 > API' 카테고리의 다른 글
| [애플] 인앱 결제 - 구독 상품 API 2 (0) | 2026.01.22 |
|---|---|
| [애플] 인앱 결제 API 2 (0) | 2026.01.17 |
| [애플] 인앱 결제 API (0) | 2026.01.17 |
| REST API (9) | 2025.08.06 |
