[애플] 인앱 결제 API 2

2026. 1. 17. 16:44·기능/API
반응형

지난 글에서는 애플 인앱 상품 중 결제 상품(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
'기능/API' 카테고리의 다른 글
  • [애플] 인앱 결제 - 구독 상품 API 2
  • [애플] 인앱 결제 - 구독 상품 API
  • [애플] 인앱 결제 API
  • REST API
그낙이
그낙이
시작은 삽질이지만, 끝은 지식입니다.
  • 그낙이
    개발 삽질 일지
    그낙이
  • 전체
    오늘
    어제
    • 분류 전체보기 (71)
      • 서버 (12)
        • 터미널 기본기 (4)
        • AWS (3)
        • Linux (5)
      • 아키텍처 (3)
      • 기능 (19)
        • 로그인 (4)
        • API (5)
        • 앱 (5)
        • 기타 (4)
      • 자유로운 개발일지 (37)
        • APP (4)
        • AI (7)
        • 직링 (19)
        • 자동매매 (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    apple developer
    티켓
    콘서트
    자동매매
    인앱 결제
    직링
    퍼피티어
    앱
    EC2
    nginx
    linux
    코인
    웹소켓
    자동화 도구
    fiddler
    apple connect store
    소셜 로그인
    개발자 도구 우회
    비트코인
    업비트
    예매
    챗봇
    챗봇 만들기
    Capacitor
    GPT
    kotlin
    IAP
    개발자 도구
    puppeteer
    FastAPI
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
그낙이
[애플] 인앱 결제 API 2
상단으로

티스토리툴바