지난 글에서는 애플 인앱 상품 중 결제 상품(In-App Purchase)의 생성 API에 대해 알아봤습니다. 이번 글에서는 심사 등록 한 이후 개별 상품 조회와, 수정 그리고 삭제 API에 대해서 알아보도록 하겠습니다. API Key, JWT 등 기본 설정 방법은 지난 글을 참고해주세요.
조회
개별 상품 가격 조회
개별 상품을 조회하기 위해서는 2가지 단계를 거쳐야합니다. 우선 등록된 앱 내에 있는 모든 개별 상품을 불러오고, 그 개별 상품 ID(iap_id)를 사용해 해당 개별 상품의 상세 정보를 받아올 수 있습니다. 만약 DB에 바로 iap_id를 저장하셨다면 1단계는 거치지 않아도 상관없지만, Apple Store Connect에서 바로 상품을 생성할 수도 있기 때문에 애플 서버와 DB 동기화를 위해서 사용하시는걸 추천드립니다. 또한 상품 상태도 확인할 수 있기 때문에 지난글에 말씀드린 것처럼 스케쥴러로 12시간씩 돌리는게 바람직해 보입니다.
async def get_apple_inapp(client: httpx.AsyncClient) -> List:
"""
Apple In-App Purchase 상품 가격 조회
1. 개별 상품 상세 조회
2. 개별 상품 가격 조회
"""
# 1. 개별 상품 상세 조회
raw_inapp_data = await get_apple_inapp_detail(client)
# 1-1. 필요한 부분만 추출
normalized_inapp_list = extract_data(raw_inapp_data)
# 2. 개별 상품 가격 조회
normalized_inapp_list_price_added = await get_apple_inapp_price(client, normalized_inapp_list)
return normalized_inapp_list_price_added
전체 개별 상품 불러오기
전체 개별 상품을 불러오기 위해서는 앱 ID(app_apple_id)가 필요합니다.
API Endpoint
GET /v1/apps/{app_apple_id}/inAppPurchasesV2
요청 예시:
async def get_apple_inapp_detail(client: httpx.AsyncClient) -> List:
"""
Apple In-App Purchase 상품 가격 조회
"""
url=f"{APPLE_STORE_BASE_URL}/v1/apps/{APPLE_STORE_APP_ID}/inAppPurchasesV2"
response = await apple_service.call_apple_api(client, "GET", url)
data = response.json()
return data["data"]
응답 예시:
{
"inAppPurchases": [
{
"id": "6757470212",
"name": "500 포인트",
"productId": "one.ios.500p",
"type": "CONSUMABLE",
"state": "APPROVED",
"familySharable": false,
"contentHosting": null,
"reviewNote": null,
"links": {
"self": "https://api.appstoreconnect.apple.com/v2/inAppPurchases/6757470212",
"localizations": "https://api.appstoreconnect.apple.com/v2/inAppPurchases/6757470212/inAppPurchaseLocalizations",
"pricePoints": "https://api.appstoreconnect.apple.com/v2/inAppPurchases/6757470212/pricePoints",
"images": "https://api.appstoreconnect.apple.com/v2/inAppPurchases/6757470212/images"
}
},
{
"id": "6757695863",
"name": "1000 포인트",
"productId": "one.ios.1000p",
"type": "CONSUMABLE",
"state": "MISSING_METADATA",
"familySharable": false,
"contentHosting": null,
"reviewNote": null,
"links": {
"self": "https://api.appstoreconnect.apple.com/v2/inAppPurchases/6757695863",
"localizations": "https://api.appstoreconnect.apple.com/v2/inAppPurchases/6757695863/inAppPurchaseLocalizations",
"pricePoints": "https://api.appstoreconnect.apple.com/v2/inAppPurchases/6757695863/pricePoints",
"images": "https://api.appstoreconnect.apple.com/v2/inAppPurchases/6757695863/images"
}
}
],
"meta": {
"total": 2
}
}
응답 예시에서 볼 수 있듯 앱 내 개별 ID(iap_id), 상품명, 상품ID, 상태 등을 확인할 수 있습니다. 표는 상태에 따른 의미입니다.
| MISSING_METADATA | 가격/로컬라이즈 등 메타 데이터 누락(심사 불가) |
| READY_FOR_REVIEW | 심사 등록 가능 |
| IN_REVIEW | 심사 중 |
| APPROVED | 심사 승인 |
| REJECTED | 심사 거절 |
iap_id 값만 사용하면 개별 상품에 대한 상세 정보를 확인할 수 있으니, 필요한 데이터를 정제해줍시다.
def extract_data(data: list) -> List:
result = []
for item in data:
iap_id = item.get("id")
attr = item.get("attributes", {})
result.append({
"iap_id": iap_id,
"name": attr.get("name"),
"product_id": attr.get("productId"),
"state": attr.get("state")
})
return result
이제 만들어진 result 배열로 개별 상품에 대한 상세 정보를 요청합시다. 또한 첫 예시 코드에서 볼 수 있듯이 상품 상태가 승인(APPROVED)인 경우에만 상품 가격을 검색하도록 요청을 보냈는데 심사 승인이 나지 않았으면 사용자 화면에서 상품이 보이면 안되기 때문에 호출을 최소화하려고 조건을 걸었습니다.
개별 상품 조회
API Endpoint
Endpoint 뒤에 filter를 걸어서 원하는 지역 내에 가격만 받아오게 설정할 수 있습니다. 상품 생성 시에 한국에서만 사용 가능하게 하고, 한국 가격만 설정해뒀으니 한국 지역 내 가격만 받아오도록 합시다. 2번째 endpoint를 사용하시면 됩니다.
GET /v1/inAppPurchasePriceSchedules/{iap_id}/manualPrices
GET /v1/inAppPurchasePriceSchedules/{iap_id}/manualPrices?filter[territory]=KOR&include=inAppPurchasePricePoint,territory
요청 예시:
요청을 보내고, 변수로 받아온 normalized에 상품 가격을 추가해줍시다.
async def get_apple_inapp_price(client: httpx.AsyncClient, normalized_list: list) -> List:
for normalized in normalized_list:
iap_id = normalized["iap_id"]
url = f"{APPLE_STORE_BASE_URL}/v1/inAppPurchasePriceSchedules/{iap_id}/manualPrices?filter[territory]=KOR&include=inAppPurchasePricePoint,territory"
response = await apple_service.call_apple_api(client, "GET", url)
data = response.json()
price = None
for item in data.get("included", []):
if item.get("type") == "inAppPurchasePricePoints":
price=item["attributes"]["customerPrice"]
break
normalized["price"] = price
return normalized_list
응답 예시:
응답 예시 중 필요한 부분입니다. 여기서 customerPrice는 사용자가 실제 결제하는 금액(사용자에게 노출되어야 하는 금액이겠죠?)와 proceeds, 애플 수수료를 제외한 앱 등록자가 받는 금액입니다.
{
"inAppPurchaseId": "6757470212",
"territory": {
"id": "KOR",
"currency": "KRW"
},
"price": {
"customerPrice": 5000,
"proceeds": 3636,
"manual": true,
"startDate": null,
"endDate": null
},
"pricePoint": {
"id": "eyJzIjoiNjc1NzQ3MDIxMiIsInQiOiJLT1IiLCJwIjoiMTAwNTUifQ"
}
}
개별 상품 현지화 정보
앱 심사 때 등록한 개별 상품의 현지화 정보도 조회해올 수 있습니다. 마찬가지로 iap_id를 사용합니다. 개발에 큰 영향을 미치지 않기 때문에 상품 현지화 정보를 수정할게 아니라면 사용하지 않으셔도 무방합니다.
API Endpoint
GET /v2/inAppPurchases/{iap_id}/inAppPurchaseLocalizations
요청 예시:
async def get_localized_list(client, iap_id):
token = load_apple_jwt()
if not token:
token = generate_apple_store_jwt()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
url = f"{APPLE_STORE_BASE_URL}/v2/inAppPurchases/{iap_id}/inAppPurchaseLocalizations"
response = awiat client.get(url, headers=headers)
data = response.json()
응답 예시:
{
"data": [
{
"type": "inAppPurchaseLocalizations",
"id": "e3c10d44-3a65-468d-aa5c-f97f6c2f7180",
"attributes": {
"name": "500 포인트",
"locale": "ko",
"description": "앱 내에서 사용할 수 있는 500포인트",
"state": "APPROVED"
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/inAppPurchaseLocalizations/e3c10d44-3a65-468d-aa5c-f97f6c2f7180"
}
}
],
"links": {
"self": "https://api.appstoreconnect.apple.com/v2/inAppPurchases/6757470212/inAppPurchaseLocalizations"
},
"meta": {
"paging": {
"total": 1,
"limit": 50
}
}
}
여기서 받은 id를 localization_id로 사용합니다. 상품 현지화 정보 수정할 때 사용합니다. 이외에도 API를 찾아보면 다양한 조회가 가능하겠지만, 가격 조회를 제외한 나머지는 크게 의미가 없어 보여 넘어가도록 하겠습니다.
수정
수정은 심사 전에만 가능한 수정이 존재합니다. 심사 승인 전에 필요한 부분이 있으면 미리미리 수정해두는 습관을 기르도록 합시다.
상품 정보 수정
상품(메타 데이터 제외)를 수정할 수 있는 API입니다. 상품명, 내부 심사를 위한 노트(reviewNote), 가족 공유 여부(boolean)을 수정할 수 있습니다. 상품명을 DB와 맞춰 사용자에게 보여준다면 상품명 수정 정도만 의미가 있겠네요.
API Endpoint
PATCH /v2/inAppPurchases/{iap_id}
요청 예시:
수정의 경우 사용자가 원하는 상품명과 설명을 변수로 받아오도록 합시다.(가족 공유 여부를 누가 수정합니까?) attributes는 필수 파라미터가 아니기 때문에 수정을 원하는 부분만 사용하셔도 무방합니다.
async def update_apple_inapp(
client: httpx.AsyncClient,
iap_id: int,
product_name: str,
) -> None:
url = f"{APPLE_STORE_BASE_URL}/v2/inAppPurchases/{iap_id}"
payload = {
"data": {
"type": "inAppPurchases",
"id": iap_id,
"attributes": {
"name": product_name,
}
}
}
response = await apple_service.call_apple_api(client, "PATCH", url, json=payload)
response.raise_for_status()
return
응답 예시:
{
"data": {
"type": "inAppPurchases",
"id": "6757470212",
"attributes": {
"name": name,
"productId": "one.ios.500p",
"inAppPurchaseType": "CONSUMABLE",
"state": "APPROVED",
"reviewNote": description,
"familySharable": false,
"contentHosting": null
},
/* 사용하지 않는 응답값들 */
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v2/inAppPurchases/6757470212"
}
}
적용 영역:


상품 현지화 정보
API를 통해 특정 국가 내 상품명과 상품 설명을 수정할 수 있습니다. 위 상품 현지화 정보 조회를 통해 얻은 localization_id를 사용합니다. 상품 현지화 정보 수정은 심사 승인 전에만 가능합니다.
API Endpoint
PATCH /v1/inAppPurchaseLocalizations/{localization_id}
요청 예시:
async def update_apple_inapp_localization(client, localization_id):
token = load_apple_jwt()
if not token:
token = generate_apple_store_jwt()
url = f"{APPLE_STORE_BASE_URL}/v1/inAppPurchaseLocalizations/{localization_id}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {
"data": {
"type": "inAppPurchaseLocalizations",
"id": localization_id,
"attributes": {
"name": "500 point",
"description": "500 point"
}
}
}
response = await client.patch(url, headers=headers, json=payload)
data = response.json()
응답 예시:
{
"data": {
"type": "inAppPurchaseLocalizations",
"id": "e07361cf-d0b6-496e-811c-e44d622c6e75",
"attributes": {
"name": "500 point",
"locale": "ko",
"description": "500 point",
"state": "WAITING_FOR_REVIEW"
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/inAppPurchaseLocalizations/e07361cf-d0b6-496e-811c-e44d622c6e75"
}
},
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/inAppPurchaseLocalizations/e07361cf-d0b6-496e-811c-e44d622c6e75"
}
}
적용 영역:

상품 가격 수정 및 상품 지역 수정
상품 가격과 상품 지역 수정은 생성과 동일한 API를 사용합니다. 지난 글에서 다룬 내용이기 때문에 간단하게 넘어가겠습니다.
상품 가격 수정
1단계: 가격에 맞는 가격 ID 받아오기
GET /v2/inAppPurchases/{iap_id}/pricePoints?filter[territory]=KOR&include=territory&limit=100
요청 예시:
async def get_apple_inapp_price_point_id(
client: httpx.AsyncClient,
iap_id: int,
price: int) -> Optional[str]:
"""
Apple In-App Purchase 사용 가능 가격 조회
"""
url = f"{APPLE_STORE_BASE_URL}/v2/inAppPurchases/{iap_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
2단계 가격 설정하기
POST /v1/inAppPurchasePriceSchedules
요청 예시:
async def set_apple_inapp_price(
client: httpx.AsyncClient,
iap_id: int,
price_point_id: str
) -> None:
"""
Apple In-App Purchase 개별 상품 가격 설정
"""
url = f"{APPLE_STORE_BASE_URL}/v1/inAppPurchasePriceSchedules"
payload = {
"data": {
"type": "inAppPurchasePriceSchedules",
"relationships": {
"baseTerritory": {
"data": {
"type": "territories",
"id": "KOR",
}
},
"inAppPurchase": {
"data": {
"type": "inAppPurchases",
"id": iap_id,
}
},
"manualPrices": {
"data": [
{
"type": "inAppPurchasePrices",
"id": "${price1}"
}
]
}
},
},
"included": [
{
"type": "inAppPurchasePrices",
"id": "${price1}",
"attributes": {},
"relationships": {
"inAppPurchaseV2": {
"data": {
"type": "inAppPurchases",
"id": iap_id
}
},
"inAppPurchasePricePoint": {
"data": {
"type": "inAppPurchasePricePoints",
"id": price_point_id
}
}
}
}
]
}
response = await apple_service.call_apple_api(client, "POST", url, json=payload)
response.raise_for_status()
return
상품 지역 수정
POST /v1/inAppPurchaseAvailabilities
요청 예시:
async def create_apple_inapp_availability(
client: httpx.AsyncClient,
iap_id: int
):
"""
Apple In-App Purchase 사용 가능 지역 설정
"""
url = f"{APPLE_STORE_BASE_URL}/v1/inAppPurchaseAvailabilities"
payload = {
"data": {
"type": "inAppPurchaseAvailabilities",
"attributes": {
"availableInNewTerritories": False,
},
"relationships": {
"availableTerritories": {
"data": [{
"type": "territories",
"id": "KOR",
}]
},
"inAppPurchase": {
"data": {
"type": "inAppPurchases",
"id": iap_id,
}
}
}
}
}
response = await apple_service.call_apple_api(client, "POST", url, json=payload)
response.raise_for_status()
return
삭제
마지막으로 삭제입니다. 삭제의 경우 상품 내 메타데이터 삭제는 수정으로 처리할 수 있으므로 상품 자체의 삭제만 다루도록 하겠습니다. 참고로 상품 삭제는 심사 승인 전에만 가능합니다. 한번 심사 승인 난 상품을 삭제처리 하고 싶다면 DB에 활성화 여부로 사용자에게 보여주면 되겠습니다.
API Endpoint
DELETE /v2/inAppPurchases/{iap_id}
요청 예시:
async def delete_apple_inapp(client, iap_id):
url = f"{APPLE_STORE_BASE_URL}/v2/inAppPurchases/{iap_id}"
response = await apple_service.call_apple_api(client, "DELETE", url)
response.raise_for_status()
이번 글에서는 애플 인앱 결제 개별 상품의 조회와 수정, 그리고 삭제까지 알아봤습니다. 심사 승인이 난 이후에 사용할 수 없는 API도 많고, 과정도 생성보다는 훨씬 적네요. 다음 글에서는 인앱 결제의 두 번째 종류인 구독 상품(Auto-Renewable Subscriptions)에 대해 알아보도록 하겠습니다.
언제나처럼 ㅡ 시작은 삽질이지만, 끝은 지식입니다.
'기능 > API' 카테고리의 다른 글
| [애플] 인앱 결제 - 구독 상품 API 2 (0) | 2026.01.22 |
|---|---|
| [애플] 인앱 결제 - 구독 상품 API (0) | 2026.01.21 |
| [애플] 인앱 결제 API (0) | 2026.01.17 |
| REST API (9) | 2025.08.06 |
