저는 하루의 대부분을 Undelivered Consumption Percentage Non Zero Error 때문에 날려버렸습니다. 어려운 오류이기 때문이 아닙니다. Delivery Status를 DELIVERED가 아닌 값으로 설정하고 Consumption Percentage를 60000으로 그대로 두었기 때문인데, Apple은 이 조합을 별다른 고민 없이 거부합니다.
이게 바로 실제 질문으로 들어가는 우회적인 경로입니다.
Apple 환불 요청에 자동으로 응답하는 방법을 고민하고 있다면, 대부분의 팀이 생각하는 것보다 작은 작업입니다. 어려운 부분은 API 호출 자체가 아니었습니다. 그 안에 넣을 데이터를 갖추는 것이 어려운 부분입니다. 필드별 상세 안내는 Apple CONSUMPTION_REQUEST explained에서 확인할 수 있습니다. 이 글은 아무도 밤새 지켜보지 않아도 되도록 이를 자동으로 연결하는 방법에 관한 것입니다.
핵심 요약
Apple 환불 자동화는 webhook 하나와 PUT 요청 하나로 이루어집니다. Apple이 CONSUMPTION_REQUEST를 보내면 12시간 이내에 응답하면 됩니다.
App Store Connect API 키가 아니라 In-App Purchase 키가 필요합니다. 둘은 서로 대체할 수 없습니다.
다섯 개 필드 중 세 개는 필수이며, 그중 두 개는 오류가 발생해야만 드러나는 제약 조건을 가지고 있습니다.
고객의 동의가 없다면 전혀 응답해서는 안 되며, 동의는 사후에 받을 수도 없습니다.
엔드포인트는 v2로 이전되었습니다. 프로덕션 환경은 api.storekit.apple.com이며, 샌드박스에서는 12시간이 아니라 5분이 주어집니다.
개발자를 위한 Apple 환불 요청이 실제로 작동하는 방식
고객이 reportaproblem.apple.com에 접속해 지난 90일 이내의 구매 내역을 찾고, 이유를 선택한 후 제출합니다. App Store Server Notifications V2를 설정해 두었다면, 여러분의 엔드포인트는 거래 정보, 제품 정보, 고객이 선택한 이유를 담은 서명된 JWS payload 형태의 CONSUMPTION_REQUEST를 받게 됩니다.
이 webhook이 Apple Store 앱 내 구매 환불의 전체 트리거입니다. 대시보드에는 아무것도 표시되지 않고, 확인할 큐도 없습니다. 그래서 많은 팀이 이미 몇 주 동안 이로 인해 돈을 잃고 나서야 이 시스템의 존재를 알게 됩니다.
그러고 나면 12시간이 주어집니다.
여기저기서 반복되는 내용이라 바로잡을 필요가 있습니다. 이는 소모성(consumable) 상품에만 국한되지 않습니다. Apple의 현재 문서에 따르면 CONSUMPTION_REQUEST는 소모성, 비소모성(non-consumable), 비갱신 구독(non-renewing subscription), 자동 갱신 구독(auto-renewable subscription) 등 모든 제품 유형에 대해 발생합니다. 오래된 가이드에는 여전히 소모성 상품과 구독에만 해당된다고 나와 있는데, 이는 최신 정보가 아닙니다.
구체적으로 "응답"이란 무엇을 의미하는가
하나의 엔드포인트에 JSON 본문을 PUT으로 전송합니다.
PUT https://api.storekit.apple.com/inApps/v2/transactions/consumption/{transactionId}
샌드박스는 api.storekit-sandbox.apple.com이며 경로는 동일합니다. 성공 시 200이 아니라 202 Accepted가 반환되는데, 상태 코드를 검증하는 과정에서 이 부분 때문에 실수하는 경우가 많습니다.
인증은 In-App Purchase 키로 서명한 단기 ES256 JWT를 통해 이루어지며, 이 키는 App Store Connect의 Users and Access 내 Integrations에서 생성합니다. App Store Connect API 키가 아닙니다. .p8 파일은 생성 시 단 한 번만 다운로드할 수 있으므로, 분실했다면 새로 생성하고 이를 사용하는 모든 곳을 교체해야 합니다. 제가 어떻게 아는지는 묻지 마세요.
다섯 개의 필드, 그리고 발목을 잡는 두 개
본문은 Consumption Request입니다. 세 개는 필수이고 두 개는 선택입니다.
Customer Consented (필수). 실제 동의가 있는 경우에만 true로 설정합니다. 이 값이 전체 프로세스를 좌우하므로 아래에서 자세히 다룹니다.
Delivery Status (필수). 구매한 상품이 정상적으로 전달되고 작동했는지를 나타냅니다.
Sample Content Provided (필수). 구매 전에 무료 샘플, 체험판, 또는 기능 정보가 제공되었는지를 나타냅니다.
Consumption Percentage (선택). 밀리유닛(milliunit) 단위로 얼마나 소비했는지를 나타냅니다. 0에서 100000 사이의 값이며, 예를 들어 45.2%는 45200입니다.
Refund Preference (선택). GRANT_FULL, GRANT_PRORATED, DECLINE, NO_PREFERENCE 중 하나입니다.
각각 오후 반나절을 잡아먹을 두 가지 제약 조건이 있습니다.
Delivery Status가 DELIVERED가 아니라면 Consumption Percentage는 반드시 0이어야 합니다. 그 외의 값을 넣으면 Undelivered Consumption Percentage Non Zero Error가 발생합니다. 제가 하루를 날린 이유입니다.
그리고 자동 갱신 구독에 대해 GRANT_PRORATED를 보낼 경우에는 consumptionPercentage를 전혀 포함하지 말아야 합니다. Apple이 결제 주기 내 경과 시간을 기준으로 이를 직접 계산하며, 포함할 경우 Consumption Percentage Auto Renewable Subscription Error로 거부됩니다. 적어도 오류 이름은 설명이 명확한 편인데, 예상보다 나은 부분이었습니다.
동의는 형식적 절차가 아니라 관문입니다
이 부분에서 Apple의 문서는 유독 직설적입니다. 고객이 동의하지 않았다면 알림에 응답하지 마세요. Customer Consented를 false로 설정해 응답하는 것이 아니라, 아예 응답하지 말라는 뜻입니다.
또한 요청이 도착한 후에 동의를 받으러 갈 수도 없습니다. 동의는 사전에 존재해야 하므로, 관련 조항은 이 기능을 출시하기 전에 이용약관이나 개인정보 보호정책에 포함되어 있어야 하며, 첫 환불 분쟁이 생긴 뒤에 추가해서는 안 됩니다. Apple의 가이드라인에 따르면 동의는 자유롭게, 구체적으로, 충분한 정보를 바탕으로, 명확하게 이루어져야 하고, 옵트인 방식이 옵트아웃 방식보다 우선하며, 동의를 받는 책임은 전적으로 개발자에게 있습니다.
실무적으로는, 환불 요청 검토를 돕기 위해 구매 및 사용 데이터를 Apple과 공유할 수 있다는 문구와 이를 철회할 수 있는 방법을 함께 명시하면 됩니다. 반드시 자체 법률 검토를 받으세요. 저는 변호사가 아니며 이는 법률 자문이 아닙니다.
개발자가 App Store 환불 요청을 자동화하는 방법
제가 구축한다면 다음 순서로 파이프라인을 만들 것입니다.
App Store Connect의 App Information에서 Production Server URL을 설정하고 Version 2를 선택합니다. Consumption 요청은 V1으로는 전달되지 않습니다.
단 하나의 필드라도 신뢰하기 전에 Apple의 루트 인증서를 기준으로 JWS 서명 체인을 검증합니다.
거래를 사용자와 매칭합니다. 구매 시점에 첨부한 appAccountToken이 가장 빠른 방법입니다. 이것이 없다면 제품과 날짜로 역추적해야 하는데, 이는 금세 복잡해집니다.
동의 여부를 확인합니다. 동의가 없으면 응답하지 않습니다. 기록을 남기고 다음으로 넘어갑니다.
실제 사용량을 가져옵니다. 추정값이나 기본값이 아닙니다. "이 사용자가 얼마나 사용했는가"에 답할 수 없다면, 그것이 바로 자동화의 진짜 공백입니다.
앞서 설명한 두 가지 제약 조건을 적용해 ConsumptionRequest를 구성합니다.
PUT 요청을 보내고, 202 응답을 확인하고, 보낸 payload를 저장합니다.
대부분의 시도는 5단계에서 멈춥니다. API 호출 자체는 반나절이면 끝나는 작업입니다. 7주 전에 이루어진 구매에 대해 사용자별로 정확한 소비 데이터를 확보하는 것은 아직 풀지 못했을 수도 있는 데이터 모델링 문제입니다.
프로덕션 환경에서 안정적으로 작동하게 만드는 요소
정상 흐름(happy path) 외에 신경 써야 할 네 가지가 있습니다.
멱등성(Idempotency). Apple은 알림을 재전송하며, 핸들러가 동일한 알림을 두 번 받게 됩니다. 알림 UUID를 기준으로 중복을 제거하지 않으면 중복 응답을 제출하게 됩니다.
실제로 담당자에게 알림이 가는 경보 체계. 조용히 실패하는 핸들러는 핸들러가 없는 것보다 더 나쁩니다. 왜냐하면 보호받고 있다고 믿게 되기 때문입니다.
샌드박스 테스트 하네스. 샌드박스는 12시간이 아니라 5분을 주므로 손으로 대충 테스트할 수 없습니다. 스크립트로 자동화하세요.
그리고 케이스별로 보낸 내용을 기록해 두어야 합니다. 환불이 거부되었을 때 어떤 preference를 제출했고 그 이유가 무엇인지 알아야 합니다. DECLINE을 보냈는데도 환불이 승인되었을 때도 그 사실을 알아야 합니다.
솔직한 예상: webhook 연결 작업을 전에 해본 적이 있다면 며칠 정도의 연결 작업이 필요하고, 이를 신뢰할 수 있게 만드는 데는 몇 주가 걸립니다. 그 대부분은 Apple 쪽 문제가 아닙니다.
아니면 다른 솔루션에 맡기기
이미 RevenueCat이나 Adapty를 사용하고 있다면, 먼저 이들이 제공하는 기능을 확인해 보세요. 둘 다 환불 응답 기능을 추가했고, 영수증과 고객 매핑 데이터가 이미 갖춰져 있습니다.
목적에 맞게 설계된 Apple 환불 관리 소프트웨어는 여기서 한 걸음 더 나갑니다. Refund Sensor는 이미 가지고 있는 App Store Connect 키로 작동하며, 기한 내에 거래 증거를 담아 모든 요청에 응답하고, 케이스별 결과를 기록합니다. SDK도 필요 없고, 코드 변경도 필요 없으며, 연동에 걸리는 시간은 약 5분입니다. 비교하며 둘러보고 싶다면 Apple refund management tools에서 더 넓은 범위의 제품을 비교한 내용을 확인하세요.
자주 질문받는, 고객 입장에서의 절차
App Store 구매에 대해 환불을 요청하는 방법을 사용자 입장에서 살펴보면: reportaproblem.apple.com에 로그인하고, 지난 90일 이내의 구매 내역을 찾아 이유를 선택합니다. Apple은 고객에게 48시간 이내에 답변을 받게 될 것이라고 안내합니다.
StoreKit의 refund request sheet를 사용하면 앱 안에서 동일한 양식을 보여줄 수 있으며, 불만을 가진 사용자를 별점 1점 리뷰 대신 Apple로 보낼 수 있습니다. 다만 이 기능이 환불을 승인할 수는 없습니다. 환불은 오직 Apple만 승인할 수 있습니다. Apple Support's guide to requesting a refund에서 고객이 보게 되는 화면을 확인할 수 있습니다.
App Store 환불 관리가 과장되게 홍보되는 경우가 많으므로 분명히 말해두자면, 여러분은 환불을 승인하거나 거부할 수 없습니다. Apple이 결정을 내리는 동안 증거를 제시하고 preference를 밝힐 수 있을 뿐입니다. 이것이 여러분이 가질 수 있는 영향력의 전부이며, 대부분의 개발자가 활용하는 것보다는 많은 편입니다.
참고 자료
자주 묻는 질문
네. 바로 이를 위해 CONSUMPTION_REQUEST 알림과 Send Consumption Information 엔드포인트가 존재합니다. 환불을 승인하거나 거부할 수는 없지만, Apple은 개발자가 보낸 정보를 결정의 입력값으로 사용합니다.
App Store Server Notifications V2를 수신하고, JWS 서명을 검증하고, 거래를 사용자와 매칭하고, 동의 여부를 확인하고, 실제 소비 데이터를 구성한 다음, 12시간 이내에 v2 consumption 엔드포인트에 PUT 요청을 보내면 됩니다. 그 외 나머지는 멱등성 처리, 알림, 기록 관리입니다.
Users and Access의 Integrations에서 발급하는 In-App Purchase 키가 필요합니다. App Store Connect API 키로는 이 호출을 인증할 수 없으며, 시계 문제처럼 보이는 401 오류가 발생합니다.
아니요, 선택 사항입니다. 하지만 deliveryStatus가 DELIVERED가 아니라면 반드시 0이어야 하며, 프로레이션(prorated) 자동 갱신 구독의 경우에는 이 값을 완전히 생략해야 합니다.
응답하지 마세요. Apple의 가이드라인은 이 부분을 명확히 규정하고 있으며, 요청이 도착한 후에는 동의를 받을 수 없습니다.
보장이 아니라 하나의 입력값입니다. Apple은 다양한 요소를 사용한다고 밝히고 있습니다. 실제로는, 많이 소비된 구매에 대해 근거가 충분한 DECLINE 응답을 제출하면 아무도 응답하지 않은 요청보다 훨씬 더 자주 반려됩니다.



