고객의 App Store 환불 내역 전체를 반환하는 엔드포인트가 하나 있는데, 그것이 무엇을 돌려주는지 설명한다
Apple의 Get Refund History 엔드포인트는 고객의 전체 App Store 환불 내역을 서명된 트랜잭션으로 반환합니다. 모든 필드, revision 토큰이 어떻게 페이지를 나누는지, 왜 앱 단위가 아니라 고객 단위인지, 그리고 놓친 환불이 얼마의 손실인지 설명합니다.

핵심 요약
- Get Refund History는 App Store Server API 엔드포인트로, 고객이 여러분의 앱에서 환불받은 인앱 구매를 서명된 트랜잭션 목록으로 반환합니다. 덕분에 알림이 도착하지 않았더라도 환불을 대조하고 접근 권한을 취소할 수 있습니다.
- 해당 고객의 아무 트랜잭션 id로 GET /inApps/v2/refund/lookup/{transactionId}를 호출하면, Apple은 여러분이 물어본 한 건만이 아니라 앱 내 모든 구매 유형에 걸친 그 고객의 환불을 반환합니다.
- 응답에는 세 개의 필드가 있습니다. signedTransactions는 페이지당 최대 20건의 JWS 트랜잭션으로, 오래된 환불부터 정렬됩니다. 여기에 페이징용 revision 토큰과 hasMore 불리언이 더해집니다.
- 마지막 revision 토큰을 저장하세요. 다음번에 그것을 되돌려 전달하면 Apple은 그 시점 이후의 환불만 반환하므로, 전체 내역을 덤프하던 것이 실행할 때마다 짧은 신규 목록으로 바뀝니다.
- 디코딩된 각 트랜잭션에는 revocationDate와 revocationReason이 담깁니다. revocationReason이 1이면 고객이 여러분 앱의 실제 또는 인지된 문제 때문에 환불했다는 뜻이고, 0이면 실수 구매 같은 다른 이유를 뜻합니다.
- 이 엔드포인트는 앱 단위가 아니라 고객 단위입니다. 앱 전체의 모든 환불을 나열하는 단일 호출은 없으므로, 트랜잭션 id에서 출발해 계정 단위로 대조하거나 앱 전체 관점을 위해서는 REFUND 알림 피드를 읽습니다.
- 이것을 연결하는 이유는 돈입니다. 결코 잡아내지 못한 환불은 계정을 계속 살려 두고, App Store가 이미 보상한 고객을 위해 여러분은 연산, 모델 API 호출, 스토리지, 정산을 계속 지불합니다.
여러분의 앱에 대해 Apple은 고객 계정에서 승인한 모든 환불의 조회 가능한 기록을 보관하며, 한 번의 호출로 그것을 반환합니다. 그 엔드포인트가 Get Refund History이며 App Store Server API의 일부입니다. 그것은 해당 고객의 전체 App Store 환불 내역을 서명된 트랜잭션 목록으로 돌려줍니다. 트랜잭션 id를 전달하면 Apple이 무엇을 환불했는지 돌려받고, 여러분은 그것을 아직 켜 둔 것과 대조합니다.
왜 이 수고를 들이는가. 보이지 않는 환불은 계속 값을 치르고 있는 환불입니다. 돈은 이미 사라졌지만 계정은 살아 있고, 살아 있는 매 시간마다 여러분은 연산, 모델 API 호출, 스토리지, 그리고 그 고객에 묶인 모든 정산에 계속 지출합니다. 환불 알림은 그것이 일어나는 순간 잡아내기 위한 것입니다. Get Refund History는 알림이 작동하지 않을 때의 최후의 보루입니다. 장애가 난 뒤, webhook를 떨어뜨린 배포 뒤, 또는 한 번의 호출로 전체 그림이 필요한 지원 건에서 씁니다.
App Store 환불 내역 엔드포인트가 반환하는 것
App Store Server API에 대해 GET /inApps/v2/refund/lookup/{transactionId}를 호출합니다. 다른 모든 호출과 동일한 JWT로 서명합니다. 경로의 트랜잭션 id는 그 고객의 아무 트랜잭션이어도 됩니다. Apple은 그것을 필터가 아니라 신원으로 읽고, 여러분의 앱 전체에 걸친 그 고객의 환불된 구매를 반환합니다. 소모성, 비소모성, 자동 갱신 및 비갱신 구독을 가리지 않습니다. 이 엔드포인트의 더 오래된 V1은 한 번의 응답에서 최대 50건의 환불을 반환했으며 이제 사용 중단되었습니다. 현재 버전은 페이지를 나누므로, 내역이 긴 고객도 거대한 페이로드 없이 처리합니다.
응답은 세 개의 필드
| Field | 담고 있는 것 |
|---|---|
| signedTransactions | 이 고객의 최대 20건 환불된 트랜잭션. 각각 검증하고 디코딩하는 서명된 JWS. revocationDate 기준으로 오래된 환불부터 정렬됨. 빈 배열은 그 고객이 여러분 앱에서 환불이 없음을 뜻함 |
| revision | 페이징 토큰. 되돌려 전달해 다음 페이지를 얻고, 마지막 하나를 보관해 다음번에는 신규 환불만 가져옴 |
| hasMore | Apple이 이 페이지가 반환한 것보다 더 많은 환불 트랜잭션을 보유할 때 True. 그럴 때는 revision을 붙여 다시 호출함 |
환불된 트랜잭션 한 건이 알려 주는 것
signedTransactions의 각 항목은 JWS입니다. Apple의 인증서 체인으로 검증하고 디코딩하면, 환불 필드가 채워진 일반 트랜잭션 페이로드를 얻습니다. 여기서 중요한 것은 다음입니다.
| Field | 알려 주는 것 |
|---|---|
| transactionId | 환불된 트랜잭션의 id. 여러분이 기록한 구매로 되돌아가는 조인 키 |
| originalTransactionId | 체인에서 첫 구매의 id. 구독의 갱신들을 하나로 묶는 방법 |
| productId | 환불된 제품. 올바른 권한만 취소하고 다른 것은 건드리지 않기 위함 |
| revocationDate | Apple이 그 트랜잭션을 환불한 UNIX 시간(밀리초 단위) |
| revocationReason | Apple이 환불한 이유. 1은 여러분 앱의 실제 또는 인지된 문제, 0은 실수 구매 같은 다른 이유 |
| price, currency | 금액(milliunits 단위)과 그 ISO 4217 통화 코드. 반환된 금액을 합산할 수 있음 |
| appAccountToken | 구매 시 부착한 UUID. 환불을 여러분 자신의 사용자로 매핑하는 가장 깔끔한 방법 |
revision 토큰이야말로 목록 전체를 반복해 다시 읽지 않게 하는 방법
이 엔드포인트를 쓰는 순진한 방법은 매번 고객을 조회하고 모든 페이지를 걸어가는 것입니다. 그것도 동작하지만, 환불이 50건인 고객에게는 이미 알던 50줄에 새로운 한 줄이 더해질 뿐입니다. revision 토큰은 그 낭비를 없애기 위해 존재합니다. 각 응답은 revision을 담습니다. hasMore가 true일 때 그것을 되돌려 전달해 다음 페이지를 얻습니다. 끝에 도달하면, 본 마지막 revision을 보관합니다.
이 엔드포인트가 하지 않는 것
이것을 토대로 삼기 전에 버려야 할 기대가 하나 있습니다. Get Refund History는 앱 단위가 아니라 고객 단위입니다. 지난주 여러분 앱이 받은 모든 환불을 그것에 물을 수는 없습니다. 그것이 답하는 것은 하나의 질문, 즉 이 계정에 어떤 환불이 있는가뿐이며, 묻기 위해서는 그 계정의 트랜잭션 id를 들고 와야 합니다. 개발자들은 끊임없이 이 벽에 부딪히고, 존재하지 않는 앱 전체 환불 엔드포인트를 찾아 나섭니다.
앱 전체 관점은 다른 곳에 있습니다. 여러분의 App Store Server Notifications 피드는 Apple이 각 환불을 승인하는 순간 REFUND 알림을 보내고, Get Notification History는 날짜 범위에 걸쳐 환불 유형으로 필터링해 그 피드를 재생하게 해 줍니다. 그래서 구분은 깔끔합니다. 알림과 그 내역은 앱 전체 스트림을 줍니다. Get Refund History는 필요할 때 한 계정의 권위 있는 목록을 줍니다. 그것이야말로 지원 데스크에서 또는 장애 뒤에 원하는 것입니다.

놓친 환불이 돈으로 얼마의 손해인가
엔드포인트는 배관입니다. 청구서야말로 그 관을 까는 이유입니다. 그 목록의 모든 환불은 이미 돌려준 돈이며, 여러분 손에 남은 유일한 변수는 더 이상 지불하지 않는 계정에 얼마나 오래 계속 지출하느냐입니다.
환불된 계정을 서비스하려고 계속 지불하고 있다
Apple이 환불을 승인하는 순간 구매 대금은 사라집니다. 계속 도는 것은 제공 비용입니다. 사용자마다 실제 작업을 하는 앱이라면, 그것은 연산, 모델 API 호출, 스토리지, 그리고 사용에 묶인 모든 크리에이터나 파트너 정산입니다. 접근을 한 번도 끊지 않은 환불된 고객은 여러분이 자비로 대는 구독입니다. Get Refund History에 대해 대조하고 찾아낸 것에 따라 취소하는 것이, 알림이 새어 나갔을 때 그 계량기를 끄는 방법입니다.
환불 사유 1은 변장한 결함 보고서
revocationReason은 무시하면 두 번 대가를 치릅니다. 첫 번째 대가는 환불 그 자체. 두 번째는 같은 원인에서 나오는 미래의 모든 환불입니다. 한 제품이 revocationReason 1, 곧 여러분 앱의 실제 또는 인지된 문제를 달고 계속 돌아온다면, Apple은 무엇이 고객에게 돈을 돌려달라고 하게 만드는지에 대한 라벨 붙은 표본을 여러분에게 건네고 있는 것입니다. 제품별로 추세를 살피면, 환불을 한 건씩 지불해 내보내는 대신 그 새는 곳을 막을 수 있습니다.
늦게라도 잡는 편이 못 잡는 것보다 훨씬 낫다
차지백은 은행과의 사이에서 최종적이며, 다른 스토어에서는 이제 개발자가 부담하는 수수료까지 딸립니다. App Store 환불은 그렇지 않습니다. 정산은 끝났지만, 권한은 아는 순간 취소할 수 있는 여러분의 것입니다. 그래서 이 엔드포인트를 통해 며칠 늦게 찾아낸 환불이라도 찾을 가치가 있습니다. 돈은 되찾을 수 없지만, 그 뒤에서 아직 돌던 지출은 멈출 수 있습니다.
이것이 알림과, 그리고 Google과 어떻게 맞물리는가
각 조각을 하나의 시스템으로 생각하세요. REFUND 알림은 실시간 신호로, Apple이 판단하는 대로 서버로 푸시됩니다. Get Refund History는 단일 고객을 위한 풀 방식의 신뢰할 수 있는 출처로, 푸시가 실패했을 때 또는 사람이 계정 전체를 눈앞에 두어야 할 때 하는 호출입니다. Google Play 쪽에서는 형태가 같은 발상에 이름만 다릅니다. VoidedPurchaseNotification이 실시간으로 푸시되고, Voided Purchases API가 여러분이 풀하는 목록입니다. 두 스토어 모두 스트림과 장부를 줍니다. 실수는 스트림만 믿는 것입니다. 스트림은 떨어지기 때문입니다.
RefundHalt 방식으로 연결하기
모든 조각이 제자리에 있으면 그 루프는 작아집니다. REFUND 알림을 트리거로 받습니다. Get Refund History에 대해 대조해, 떨어진 webhook가 환불된 계정을 결코 살려 두지 않게 합니다. 각 트랜잭션을 디코딩하고, appAccountToken 또는 transactionId로 여러분 사용자에게 연결하고, revocationReason을 읽어 결함 환불이 그저 정리되는 게 아니라 표시되게 하며, 계정 전체가 아니라 정확한 권한을 취소합니다. revision 토큰으로 페이징해 오래된 것이 아니라 신규 환불을 읽습니다.
이 부분이야말로 RefundHalt가 여러분을 대신해 실행하는 것입니다. 환불 알림을 듣고, 권위 있는 목록이 필요할 때는 Get Refund History로 되돌아가며, 모든 서명된 트랜잭션을 검증하고, 정확한 구매를 취소하며, revision을 보관해 각 회차가 바뀐 것만 읽게 합니다. 폴링과 JWS 검증을 직접 세우지 않고도, 몇 초 만에 접근이 끊기고, 누가 무엇을 왜 환불받았는지에 대한 깔끔한 기록을 얻습니다.
자주 묻는 질문
- 한 고객만이 아니라 앱 전체의 모든 환불을 보려면 어떻게 하나요?
- Get Refund History로는 할 수 없습니다. 고객 단위이며 물으려는 계정의 트랜잭션 id가 필요하기 때문입니다. 앱 전체 관점을 위해서는 App Store Server Notifications 피드를 쓰세요. Apple이 각 환불을 승인할 때마다 REFUND 알림을 보냅니다. 그리고 Get Notification History로 날짜 범위에 걸쳐 환불 유형으로 필터링해 그 피드를 재생하세요.
- Get Refund History 엔드포인트는 환불을 몇 건 반환하나요?
- 현재 버전은 페이지당 최대 20건의 환불된 트랜잭션을 가장 오래된 환불을 먼저 두고 반환하며, hasMore가 true일 때 revision 토큰으로 나머지를 페이징합니다. 사용 중단된 V1 엔드포인트는 한 번의 응답에서 최대 50건을 반환했습니다. 총량에는 상한이 없으므로, 내역이 긴 고객은 단지 더 많은 페이지에 걸쳐질 뿐입니다.
- revision 토큰은 무엇을 위한 것인가요?
- 페이징하는 방법이자, 매번 고객의 전체 내역을 다시 읽지 않게 하는 방법입니다. 각 응답에는 revision이 포함됩니다. 그것을 되돌려 전달해 다음 페이지를 가져오고, 마지막 하나를 저장해 다음 조회가 그 시점 이후의 환불만 반환하게 합니다. 그러면 예약된 대조가 짧은 신규 행 목록에 머무릅니다.
- 환불된 트랜잭션의 revocationReason은 무엇을 뜻하나요?
- Apple이 그 트랜잭션을 환불한 이유입니다. 값 1은 고객이 여러분 앱 내의 실제 또는 인지된 문제 때문에 환불했음을 뜻하고, 0은 실수 구매 같은 다른 이유를 뜻합니다. revocationDate는 환불이 언제 일어났는지 UNIX 밀리초로 알려 줍니다. revocationReason을 읽으면 제품 결함과 일회성 후회 환불을 갈라낼 수 있습니다.
- 이미 REFUND 알림을 처리하고 있다면 이것이 여전히 필요한가요?
- 네, 최후의 보루로서요. 알림은 실시간 신호이지만, 푸시는 장애, 잘못된 배포, webhook 변경 중에 도착하지 못할 수 있고, 놓친 환불은 환불된 계정을 살려 둔 채 여러분에게 돈을 계속 물립니다. Get Refund History는 여러분이 대조하는 풀 방식의 신뢰할 수 있는 출처로, Apple이 이미 환불한 것이 켜진 채 남지 않게 합니다.
출처 및 추가 자료
- Apple Developer: Get Refund History (App Store Server API)
- Apple Developer: RefundHistoryResponse
- Apple Developer: JWSTransactionDecodedPayload (revocationDate, revocationReason)
- Apple Developer: Get Refund History V1 (deprecated)
- Apple Developer: App Store Server Notifications V2 notificationType (REFUND)
- Apple Developer: Support customers and handle refunds (WWDC21)
RefundHalt
App Store와 Google Play 환불 자동 처리
계속 읽기
당신의 앱은 인앱 환불 요청 시트를 띄울 수 있습니다. 고객이 제출을 누른 뒤 Apple이 무엇을 하는지 알려드립니다
Apple의 인앱 환불 요청은 고객이 앱을 떠나지 않고, Apple이 만들고 심사하는 시트에서 환불을 신청하게 해줍니다. beginRefundRequest가 무엇을 반환하는지, 서버에서 시작되는 CONSUMPTION_REQUEST와 48시간 타이머, 그리고 이 버튼을 출시할 가치가 있는지 정리합니다.
Google Play 구매가 환불되거나 지불 거절되었을 때, 그것을 알아내는 방법이 Voided Purchases API입니다
Google Play는 구매가 환불되거나 지불 거절되면 조용히 그 구매를 무효화합니다. Voided Purchases API는 그러한 주문의 목록이며, 이를 통해 접근 권한을 회수할 수 있습니다. 여기서는 모든 필드, 30일 기간, 주문을 숨기는 revoke 옵션, 그리고 그 비용을 설명합니다.