Apple이 판단을 내린 뒤 세 가지 App Store 환불 알림이 도착하고, REFUND_REVERSED는 판매를 되돌려준다
Apple은 App Store Server Notifications V2를 통해 네 가지 환불 메시지를 보내지만, 대부분의 앱은 두 가지만 처리합니다. REFUND는 권한을 회수하라고 알리고, REFUND_DECLINED는 판매를 그대로 유지하라는 뜻이며, REFUND_REVERSED는 판매를 되돌려주면서 당신이 회수한 것을 복원하라고 요청합니다. 각각 무엇이 필요한지 설명합니다.

핵심 요약
- Apple은 App Store Server Notifications V2를 통해 환불 관련 메시지 네 가지를 보냅니다. CONSUMPTION_REQUEST는 당신의 증거를 요청하고, REFUND, REFUND_DECLINED, REFUND_REVERSED는 Apple이 이미 판단을 내린 뒤 결과를 보고합니다.
- REFUND 알림은 App Store가 해당 거래를 환불했다는 뜻입니다. revocationDate와 revocationReason을 담고 있으며, 그 한 건의 거래에 묶인 권한을 회수하라는 신호이지, 해당 상품의 모든 구매를 회수하라는 것이 아닙니다.
- revocationReason에는 두 가지 값이 있습니다. 1은 당신의 상품에 문제가 있어 환불이 승인되었음을 뜻하고, 0은 다른 이유로 승인되었음을 뜻합니다. 값이 1인 경우는 기록하고 추세를 살필 가치가 있는 품질 신호입니다.
- REFUND_DECLINED는 Apple이 고객의 환불을 거절했다는 뜻입니다. 당신은 판매를 유지하고 아무것도 바꾸지 않지만, 이것이 안전한 것은 판단이 최종이 되기 전에 접근을 회수하지 않았을 때뿐입니다.
- REFUND_REVERSED는 Apple이 이전에 승인한 환불을 되돌렸다는 뜻이며, 보통 고객이 그 환불에 이의를 제기한 뒤에 일어납니다. 거래에서 회수 관련 필드가 사라지며, Apple 자체 지침은 콘텐츠를 회수했다면 복원해야 한다는 것입니다.
- 네 가지 알림 모두에 HTTP 200으로 응답하세요. 서버가 다운되어 하나를 놓쳤더라도, Get Refund History 엔드포인트를 사용하면 거래 id로 환불된 거래를 조회해 대조할 수 있습니다.
- 과거 구독 기간에 대한 환불이 반드시 접근을 종료해야 함을 뜻하지는 않습니다. 더 최근의 유료 기간이 아직 유효하다면, 오래된 거래로 회수하면 현재 결제 중인 고객을 차단하게 됩니다.
Apple이 당신의 환불에 대해 판단을 내려도, 이야기가 그것으로 끝나지는 않습니다. 결과가 확정되면 App Store는 당신의 서버에 세 가지 App Store 환불 알림 중 하나를 보내며, 각각 서로 다른 조치를 요구합니다. REFUND는 돈이 사라졌으니 접근을 회수하라고 알립니다. REFUND_DECLINED는 고객이 요청에서 졌으니 판매를 유지하라고 알립니다. REFUND_REVERSED는 Apple이 이미 승인했던 환불을 취소했으니 그 판매가 다시 당신의 것이 되고, 회수했던 것은 무엇이든 돌려줘야 한다고 알립니다. 대부분의 앱은 첫 번째만 연결해 두고 나머지 둘은 조용히 무시합니다. 이렇게 해서 결제한 고객이 자신이 값을 치른 것에서 차단당하게 됩니다.
이 셋은 CONSUMPTION_REQUEST와는 별개이며, 후자는 답신을 요구하는 유일한 환불 메시지입니다. 판단 이후의 알림은 논쟁을 원하지 않습니다. 이들이 원하는 것은 HTTP 200과 고객 접근에 대한 올바른 변경입니다. 아래에서는 각각의 의미, 사실을 담는 정확한 필드, 그리고 잘못 처리했을 때 돈이 어디서 새는지를 설명합니다.
네 가지 환불 알림, 그리고 답신을 원하는 것은 어느 것인가
App Store Server Notifications V2는 하나의 피드입니다. URL 하나에 연결하면 Apple이 모든 알림 유형을 그곳으로 보내므로, 처리하든 안 하든 당신은 이미 네 가지 환불 메시지를 모두 받고 있습니다. 그중 네 가지 유형이 환불과 관련되며, 질문인 것은 그중 하나뿐입니다.
| 알림 | Apple이 알리는 내용 | 당신의 조치 | 답신 필요 여부 |
|---|---|---|---|
| CONSUMPTION_REQUEST | 고객이 환불을 요청했고 Apple이 당신의 데이터를 원함 | 12시간 이내에 Send Consumption Information | 필요, 실제 데이터 |
| REFUND | App Store가 해당 거래를 환불함 | 그 거래의 권한을 회수 | 불필요, HTTP 200 |
| REFUND_DECLINED | App Store가 환불을 거절함 | 접근을 유지하고 아무것도 바꾸지 않음 | 불필요, HTTP 200 |
| REFUND_REVERSED | Apple이 승인했던 환불을 되돌림 | 회수했던 콘텐츠를 복원 | 불필요, HTTP 200 |
REFUND 알림이 실제로 알려주는 것
REFUND는 App Store가 고객에게 거래를 성공적으로 환불했을 때 발생합니다. 모든 구매 유형에 적용됩니다. 소비형, 비소비형, 자동 갱신 구독, 그리고 비갱신 구독입니다. 알림 내부의 서명된 거래는 이제 환불 전에는 없던 두 필드를 담고 있으며, 그 두 필드가 이야기의 전부입니다.
revocationDate와 revocationReason이 사실을 담는다
revocationDate는 App Store가 해당 거래를 환불하거나 회수한 UNIX 시간이며, 밀리초 단위입니다. revocationReason은 환불의 범주를 알려주며, 정확히 두 개의 값을 가집니다.
| revocationReason | Apple의 의미 | 여기서 읽어야 할 것 |
|---|---|---|
| 1 | 상품의 문제로 환불이 승인됨 | 품질 또는 전달 신호. 기록하고, 추세를 살피며, 특정 상품이나 특정 빌드에서 패턴을 찾으세요 |
| 0 | 다른 이유로 환불이 승인됨 | 일반적인 환불. 권한을 회수하고 넘어가세요 |
거래에 revocationDate가 존재한다는 것 자체가 표시입니다. 나중에 거래를 가져왔을 때 revocationDate가 있다면, 알림이 있든 없든 그 구매는 환불된 것입니다. 이유도 함께 읽어, 특정 릴리스에서 밀려드는 값 1 환불의 물결이 잡음으로 지나쳐 버려지지 않게 하세요.
상품 단위가 아니라 거래 단위로 회수하라
여기서의 함정은 너무 많이 회수하는 것입니다. REFUND 하나는 거래 한 건을 지목합니다. 고객이 그 상품 id로 지금까지 한 모든 구매를 비활성화하라는 뜻이 아닙니다. Apple 자체 지침은, 무언가를 끊기 전에 고객이 여전히 보유한 접근을 확인하라는 것입니다. 권한이 겹치기 때문입니다. 전형적인 사례는 구독입니다. 환불은 지난달 갱신에 발생하는데, 이번 달 갱신은 유효하고 전액 결제된 상태입니다. 상품 단위로 회수하면, 이미 끝난 기간에 대한 환불 때문에 현재 결제 중인 고객을 차단하게 됩니다.
REFUND_DECLINED는 이미 이겼다는 뜻이므로, 되돌리지 말 것
REFUND_DECLINED는 App Store가 고객의 환불 요청을 거절했을 때 도착합니다. 고객이 요청했고, Apple이 안 된다고 했으며, 당신은 판매를 유지합니다. 겉보기에는 할 일이 없고, 그것이 요점입니다. 이 알림이 드러내는 실수는 다른 것으로, 접근을 너무 일찍 회수하는 것입니다.
당신의 코드가 Apple이 판정하기 전에 CONSUMPTION_REQUEST에 반응해 고객의 접근을 회수해 버리면, REFUND_DECLINED는 그 결정이 터지는 순간이 됩니다. Apple은 당신의 돈을 남겨 두었는데, 당신은 환불이 거절된 고객을 차단해 버린 것입니다. 그 고객은 이제 쓸 수 없는 상품에 값을 치르고, 지원 티켓을 열며, 그 일을 기억합니다. 해결책은 기능이 아니라 규칙입니다. REFUND에서 회수하고, 요청에서는 결코 회수하지 마세요. REFUND_DECLINED는 이른 회수가 잘못된 판단이었을 것임을 Apple이 확인해 주는 것일 뿐입니다.
REFUND_REVERSED는 당신에게 돈을 돌려주는 알림
REFUND_REVERSED는 거의 아무도 처리하지 않는 것이며, 당신에게 돈을 돌려주는 것입니다. Apple은 이전에 승인한 환불을 되돌릴 때 이것을 보내는데, 보통 고객이 그 환불에 이의를 제기한 뒤입니다. REFUND가 거래에 추가했던 회수 필드가 다시 제거되어, 그 구매는 다시 결제된 것으로 읽힙니다. Apple은 개발자의 할 일을 한 줄로 밝힙니다. 관련 환불의 결과로 앱이 콘텐츠나 서비스를 회수했다면 복원해야 한다는 것입니다. 소비형에서 자동 갱신 구독까지 모든 구매 유형에 적용됩니다.
몇 주 뒤에 생기는 문제
개발자들이 Apple 자체 포럼에서 제기하는 진짜 질문은 시점입니다. REFUND_REVERSED는 최초의 REFUND로부터 몇 주 뒤, 구독 기간이 만료되고도 한참 지나서 도착할 수 있습니다. 그때 접근을 복원하나요? 그 거래가 실제로 부여하는 것을, 그 거래가 다루는 범위로 한정해 복원하세요. 소비형이나 비소비형이라면 잠금 해제를 다시 켜세요. 이미 지나간 구독 기간에 대해서는, 새로운 시간을 나눠 주는 것이 아니라, 기록을 바로잡아 고객의 이력을 정확하게 하고 아직 유효한 권한을 다시 활성화하는 것입니다. 특정 거래를 복원하면, 무엇이 현재 유효한지는 당신의 중첩 로직이 결정합니다.

이것을 제대로 하는 데 돈이 어디에 있는가
이 알림들 하나하나는 실제 숫자에 대응하며, 잘못 처리하는 비용은 판매 가격만이 아닙니다.
REFUND: 환불된 고객에게 서비스를 계속 제공하는 비용을 멈춰라
REFUND가 도착하는 순간 판매 가격은 사라집니다. 그래도 통제할 수 있는 것은 계속 제공하는 비용입니다. 환불된 권한이 살아 있는 시간이 1시간 늘어날 때마다, 고객이 더는 값을 치르지 않는 것들, 즉 컴퓨트, 모델 API 호출, 저장소, 그리고 그 사용에 묶인 크리에이터나 파트너 지급에 계속 돈을 씁니다. REFUND에서 즉시 회수하면 그 계량기가 멈춥니다. 이 알림을 무시하는 것은 스토어가 이미 보상해 준 사람을 위해 상품을 대주는 것을 뜻합니다.
REFUND_DECLINED: 승리를 호의성 환불로 바꾸지 마라
일찍 회수했는데 나중에 환불이 거절되면, 장부상으로는 판매를 지켰지만 실제로는 잃은 것입니다. 값을 치른 고객은 상품을 쓸 수 없으므로, 당신은 지원 대화를 떠안고 종종 이를 무마할 재량 환불까지 떠안습니다. 이는 애초에 위험하지도 않던 한 건의 판매에 두 번 값을 치르는 것입니다. REFUND_DECLINED를 올바르게 다루는 데는 아무 비용도 들지 않으며, 바로 그래서 REFUND 전까지 접근을 건드리지 않는 것이 채택할 수 있는 가장 저렴한 규칙입니다.
REFUND_REVERSED: 최악의 조합은 돈과 접근이 둘 다 사라지는 것
REFUND_REVERSED를 무시하면 나올 수 있는 최악의 결과에 이릅니다. 당신은 돈을 받았는데 고객은 아무것도 없습니다. 그들은 환불을 되돌리려고 이미 은행에 한 번 연락했고, 이제 요금이 청구되는 상품에서 차단된 사람은 두 번째로 은행에 연락할 가능성이 높습니다. 그 다음 이의는 카드 차지백이 될 수 있으며, 그것은 은행 최종 결정이고 그 판매가 준 것보다 더 큰 비용이 듭니다. REFUND_REVERSED가 도착하는 순간 접근을 복원하는 것은 환불 흐름 전체에서 가장 저렴한 보험입니다.
무엇을 연결할 것인가
모델만 맞으면 처리는 작습니다. 권한을 거래 id로 키를 삼아 모든 알림이 구매 한 건을 가리키게 하세요. CONSUMPTION_REQUEST에서는 12시간 이내에 데이터를 보냅니다. REFUND에서는 그 거래를 회수합니다. REFUND_DECLINED에서는 아무것도 하지 않습니다. REFUND_REVERSED에서는 복원합니다. 모두에 신속히 HTTP 200을 반환하고, 접근 변경은 당신의 시간에 맞춰 하세요.
알림이 남기는 틈에는 Get Refund History 엔드포인트를 사용하세요. 장애로 서버가 다운되어 REFUND를 놓쳤다면, 거래 id에 대해 App Store Server API의 환불 조회를 /inApps/v2/refund/lookup/{transactionId}에서 호출하고, revocationDate와 revocationReason을 담은 서명된 거래를 읽어 오세요. 한 번에 거래 하나씩 대조하며 고객의 환불된 구매를 페이지로 넘기므로, 놓친 webhook이 영구히 잘못 설정된 권한이 되지 않습니다.
이것이 바로 RefundHalt가 당신을 대신해 운영하는 부분입니다. 네 가지 유형을 모두 수신 대기하고, REFUND에서 회수하며, REFUND_DECLINED에서는 접근을 건드리지 않고, REFUND_REVERSED에서는 자동으로 복원하며, 각각을 정확한 거래에 키로 묶습니다. 되돌려진 환불이 결제 중인 고객을 계속 차단한 채 큐에서 대기하는 일이 없고, 거절된 환불이 나중에 되돌려야 할 회수를 유발하는 일도 결코 없습니다.
자주 묻는 질문
- REFUND와 REFUND_REVERSED의 차이는 무엇인가요?
- REFUND는 App Store가 거래를 환불했다는 뜻이며 그 권한을 회수해야 합니다. 반면 REFUND_REVERSED는 Apple이 승인한 환불을 되돌렸다는 뜻이며 회수한 콘텐츠를 복원해야 합니다. 이 둘은 한 쌍입니다. 구매는 먼저 REFUND로 가고, 이후 고객의 이의가 뒤집히면 REFUND_REVERSED로 갈 수 있습니다. 접근 변경을 거래 id로 키를 삼아, 각 알림이 올바른 구매에 작용하게 하세요.
- REFUND 알림에 대해 무언가를 돌려보내야 하나요?
- 아니요. REFUND, REFUND_DECLINED, REFUND_REVERSED에는 본문 없는 HTTP 200으로 응답합니다. 데이터 전송을 요구하는 것은 CONSUMPTION_REQUEST뿐이며, 12시간 이내에 Send Consumption Information 엔드포인트를 통해 보냅니다. 나머지 셋은 Apple이 판단을 보고하는 것이지 질문하는 것이 아닙니다.
- REFUND_DECLINED 알림을 받으면 무엇을 해야 하나요?
- 아무것도 바꾸지 않습니다. 고객의 환불이 거절되었고 당신은 판매를 유지하기 때문입니다. REFUND_DECLINED가 일을 만드는 유일한 경우는 Apple이 판정하기 전에 접근을 너무 일찍 회수했을 때입니다. CONSUMPTION_REQUEST가 아니라 REFUND에서 회수하면, REFUND_DECLINED는 접근이 올바르게 그대로 남겨졌음을 확인해 주는 것이 됩니다.
- REFUND_REVERSED가 환불 몇 주 뒤에 도착하면 접근을 복원해야 하나요?
- 네, 그 특정 거래가 부여하는 권한을 복원하세요. Apple은 관련 환불 때문에 앱이 콘텐츠를 회수했다면 복원해야 한다고 밝힙니다. 소비형이나 비소비형이라면 잠금 해제를 다시 켜세요. 이미 만료된 구독 기간에 대해서는 새로운 시간을 부여하는 것이 아니라 기록을 바로잡는 것이므로, 무엇이 현재 유효한지는 여전히 당신의 중첩 로직이 결정합니다.
- 서버가 놓친 환불 알림을 어떻게 잡을 수 있나요?
- App Store Server API의 Get Refund History 엔드포인트를 사용하세요. 이것은 /inApps/v2/refund/lookup/{transactionId}에서 거래 id로 고객의 환불된 거래를 조회합니다. revocationDate와 revocationReason을 담은 서명된 거래를 반환하므로, 장애 이후에도 이미 발생한 알림을 기다리지 않고 접근을 대조할 수 있습니다. 호출 한 번에 거래 id 하나를 처리하며 그 고객의 환불된 구매를 페이지로 넘깁니다.
출처 및 추가 자료
RefundHalt
App Store와 Google Play 환불 자동 처리
계속 읽기
이제 모든 Apple 환불 요청에는 이유가 함께 오며, consumptionRequestReason 이 그것을 읽는 방법입니다
WWDC24 이후 모든 Apple CONSUMPTION_REQUEST 는 consumptionRequestReason, 즉 고객이 직접 밝힌 환불 사유를 담고 있습니다. 값은 UNINTENDED_PURCHASE 부터 LEGAL 까지 다섯 가지이며, 각각은 12시간의 대응 창 안에서 여러분이 돌려보내는 내용을 바꿔야 합니다. 여기서 그 하나하나를 읽는 방법을 설명합니다.
Google Play의 지불 거절 심사는 반격할 24시간을 줍니다. 무엇을 보내야 하는지 알아봅시다
은행이 Google Play 결제를 취소하면 Google은 당신의 서버로 PendingRefundReviewNotification을 보내고 24시간 카운트다운을 시작합니다. ReviewRefund API를 통해 환불 선호와 실제 소비 증거로 응답하지 않으면 이 분쟁은 당신 없이 결정됩니다. 여기서 그 전체 흐름을 필드별로 살펴봅니다.