Google Play 구매가 환불되거나 지불 거절되었을 때, 그것을 알아내는 방법이 Voided Purchases API입니다
Google Play는 구매가 환불되거나 지불 거절되면 조용히 그 구매를 무효화합니다. Voided Purchases API는 그러한 주문의 목록이며, 이를 통해 접근 권한을 회수할 수 있습니다. 여기서는 모든 필드, 30일 기간, 주문을 숨기는 revoke 옵션, 그리고 그 비용을 설명합니다.

핵심 요약
- Voided Purchases API, 즉 purchases.voidedpurchases.list 메서드는 Google Play가 취소, 환불 또는 지불 거절한 주문을 반환합니다. 이를 통해 고객이 더 이상 소유하지 않는 것에 대한 접근을 끊는 회수 시스템을 구축할 수 있습니다.
- revoke된 주문만 나타납니다. revoke 옵션 없이 발행된 개발자 환불은 이 API에 보이지 않으므로, 접근 권한을 회수하려면 revoke를 켜고 환불해야 합니다.
- 기간은 30일입니다. startTime은 30일 전보다 오래되게 설정할 수 없으므로, 서버가 한 달 넘게 다운되면 그 무효화된 주문은 영원히 사라집니다. 일정에 따라 폴링하세요.
- voidedSource는 누가 주문을 무효화했는지 알려줍니다. 0은 사용자, 1은 개발자, 2는 Google입니다. voidedReason은 이유를 알려주며, 0 Other부터 7 Chargeback과 8 Unacknowledged_purchase까지 있습니다.
- Real-time developer notifications는 구매가 무효화되는 순간 VoidedPurchaseNotification을 푸시하지만, 이를 신호로 취급하세요. 회수하기 전에 권위 있는 목록을 얻기 위해 Voided Purchases API를 호출하세요.
- 구독 갱신은 purchaseToken이 아니라 orderId로 식별하세요. 하나의 purchaseToken은 구독의 모든 갱신을 포함하므로, 토큰만으로는 두 갱신을 구별할 수 없습니다.
- 할당량은 하루 6,000개의 쿼리, 그리고 30초 창 안에서 30개의 쿼리입니다. 따라서 연속 토큰으로 결과를 페이지 단위로 넘기고 시간 창으로 조회하세요. 주문마다 한 번씩 호출하는 일은 절대 하지 마세요.
Google Play에서의 환불은 당신의 문을 두드리지 않습니다. 돈은 움직이고, 고객은 앱을 열어둔 채로 있으며, 당신이 찾으러 가지 않는 한 당신 쪽에서는 아무것도 바뀌지 않습니다. Voided Purchases API가 바로 그 찾으러 가는 곳입니다. 취소, 환불 또는 지불 거절된 주문의 목록을 건네주므로, 고객이 더 이상 지불하지 않는 것에 대한 접근을 회수할 수 있습니다. 예약된 작업을 그쪽으로 향하게 하고, 목록을 읽고, 권한을 끊으세요. 그것이 이 루프의 전부입니다.
대부분의 팀이 걸려 넘어지는 함정이 하나 있는데, 그것은 코드 안에 있지 않습니다. 여기에 나타나는 것은 revoke된 주문뿐입니다. Play Console에서 revoke 옵션을 체크하지 않고 구매를 환불하면, 그 주문은 이 API에 결코 도달하지 않으며, 환불받은 고객이 당신이 판 모든 것을 그대로 지닌 채 작업은 아무 문제 없이 돌아갑니다. 이 글은 API를 필드별로, 그것을 제한하는 숫자별로, 그리고 이를 건너뛸 때 어디서 돈이 새는지 짚어봅니다.
Voided Purchases API가 실제로 반환하는 것
이 API는 하나의 질문에 답합니다. 이 앱의 어떤 주문이 최근에 무효화되었는가입니다. 무효화(void)는 고객이 돈을 돌려받는 세 가지 결과를 포함합니다. 취소, 환불 또는 지불 거절입니다. 이는 일회성 인앱 상품과 구독 모두에 적용되며, 범위는 하나의 매개변수로 선택합니다. type을 0으로 설정하면 무효화된 인앱 상품 구매만 얻으며, 이것이 기본값입니다. 1로 설정하면 무효화된 인앱 구매와 무효화된 구독 구매를 함께 얻습니다.
목록의 각 항목은 voided purchase 객체입니다. 필드는 적으며 그 하나하나가 모두 중요합니다.
voided purchase의 필드
| 필드 | 담고 있는 것 |
|---|---|
| orderId | 일회성 구매, 구독 구매 또는 단일 구독 갱신을 고유하게 식별하는 주문 id. 이것이 당신의 조인 키입니다 |
| purchaseToken | 일회성 구매 또는 구독을 식별하는 토큰. 갱신을 구별하지 않으므로 갱신에는 orderId를 사용하세요 |
| purchaseTimeMillis | 구매가 이루어진 시각. 에포크 이후 밀리초 |
| voidedTimeMillis | 구매가 취소, 환불 또는 지불 거절된 시각. 에포크 이후 밀리초 |
| voidedSource | 누가 무효화를 시작했는가. 0 사용자, 1 개발자, 2 Google |
| voidedReason | 구매가 무효화된 이유. 0에서 8까지의 정수 |
| voidedQuantity | 수량 기반 부분 환불로 인한 무효화된 수량. includeQuantityBasedPartialRefund가 true일 때만 반환됨 |
행동하기 전에 voidedReason을 읽으세요
voidedReason은 원시 목록을 결정으로 바꾸는 필드입니다. 구매자의 후회로 인한 환불과 은행의 지불 거절은 둘 다 같은 목록에 들어가지만, 그것들은 같은 사건이 아니며 8월의 요금 체계는 그중 하나를 비싸게 만듭니다. 다음이 전체 목록입니다.
| voidedReason | 라벨 | 당신에게 의미하는 것 |
|---|---|---|
| 0 | Other | 카테고리가 지정되지 않았습니다. 회수하고 넘어가세요 |
| 1 | Remorse | 구매자가 마음을 바꿨습니다. 일반적인 환불입니다 |
| 2 | Not_received | 고객이 상품을 받지 못했다고 말합니다. 배송을 확인할 가치가 있습니다 |
| 3 | Defective | 상품이 작동하지 않았습니다. 품질 신호이니 기록하세요 |
| 4 | Accidental_purchase | 의도하지 않은 구매. 공유 기기에서 흔합니다 |
| 5 | Fraud | Google이 거래를 사기로 표시했습니다 |
| 6 | Friendly_fraud | 정당한 카드 소유자가 자신이 한 청구를 이의 제기하는 지불 거절 |
| 7 | Chargeback | 고객의 은행이 결제를 되돌렸습니다. 은행에서 확정되며 이제 당신에게 청구됩니다 |
| 8 | Unacknowledged_purchase | 당신의 앱이 확인하지 않은 구매를 Google이 자동 환불했습니다 |
30일 기간은 목록을 비우는 함정입니다
Voided Purchases API는 지난 30일간의 무효화된 구매만 보여줄 수 있습니다. startTime 매개변수는 현재 시각에서 30일을 뺀 값을 기본으로 하며, 그보다 오래되게 설정할 수 없습니다. endTime의 기본값은 지금입니다. 따라서 이 엔드포인트는 아카이브가 아니라 한 달짜리 롤링 창입니다.
그 결과는 냉정합니다. 폴링 작업이 고장 나고 5주 동안 아무도 알아차리지 못하면, 첫째 주의 무효화는 API에서 만료되어 사라집니다. 그것들을 되돌릴 호출은 없습니다. 그 주문들을 회수할 수 없을뿐더러, 다른 방법으로 포착하지 않는 한 그것들이 존재했다는 사실조차 알지 못합니다. 이 API는 당신의 최악의 장애 길이에 맞춰진 구멍이 뚫린 안전망입니다.
revoke 옵션이 주문이 나타날지 여부를 결정합니다
이것은 팀이 API를 고장 났다고 보고하는 가장 흔한 이유입니다. 반환되는 것은 revoke된 주문뿐입니다. 사용자가 시작한 환불, 취소, 지불 거절, 그리고 Google이 시작한 환불은 항상 revoke되므로 항상 나타납니다. 개발자가 시작한 환불은 다릅니다. Play Console이나 Orders API를 통해 직접 주문을 환불할 때, 그것을 함께 revoke할지 여부를 선택합니다. revoke 없이 환불하면, 그 주문은 고객과의 사이에서 정산되지만 Voided Purchases API에는 결코 나타나지 않습니다.
거기서 따라오는 규칙은 단순합니다. 접근 권한을 회수하는 것이 의도라면, revoke 옵션을 켜고 환불하세요. 그러지 않으면 돈을 돌려주고 문을 열어둔 것이며, 아무리 잘 짜인 회수 작업이라도 처리할 대상이 아무것도 없습니다.
할당량에 걸리지 않고 폴링하는 방법
이 엔드포인트는 속도 제한이 있으며, 그 제한은 순진한 루프라면 걸릴 만큼 낮습니다. 하루에 6,000개의 쿼리(태평양 시간으로 집계), 그리고 30초 구간에서 30개를 넘지 않는 쿼리가 허용됩니다. 그 예산은 창 단위 폴링에는 충분하고 주문마다 한 요청씩 하는 설계에는 적대적입니다.
쿼리 창과 연속 토큰
maxResults는 기본값이 1,000이며, 이는 상한이기도 합니다. 창이 한 페이지보다 많은 무효화를 담고 있으면, 응답에는 nextPageToken을 가진 tokenPagination 객체가 들어 있습니다. 그 토큰을 다음 호출에 다시 전달하여 페이지를 넘어가세요. 관심 있는 창을 한정하기 위해 startTime과 endTime을 설정하고, 토큰이 소진될 때까지 페이지를 넘긴 다음 창을 전진시키세요. 이 패턴은 30초 버스트 제한과 하루 상한 양쪽 안에 머물게 해줍니다.
Real-time developer notifications가 틈을 메웁니다
매일 폴링해도 최대 하루의 사각지대가 남고, 30일 기간은 긴 공백을 벌합니다. Real-time developer notifications는 그 지연을 제거합니다. Google은 구매가 무효화되는 순간 당신이 소유한 Cloud Pub/Sub 토픽으로 VoidedPurchaseNotification을 게시하고, 당신의 백엔드는 몇 초 안에 그것을 소비합니다. 메시지는 작습니다.
| RTDN 필드 | 담고 있는 것 |
|---|---|
| purchaseToken | 원래 구매에서 온 토큰 |
| orderId | 무효화된 거래의 주문 id. 구독 갱신마다 새로운 것 |
| productType | 구독은 1, 일회성 구매는 2 |
| refundType | 전액 환불은 1, 수량 기반 부분 환불은 2 |

이것이 돈으로 당신에게 무엇을 초래하는가
이 API는 배관이지만, 그것을 연결하는 이유는 청구서입니다. 그 목록의 모든 무효화는 실제 숫자에 대응하며, 그중 둘은 점점 더 비싸지고 있습니다.
지불 거절 청구서는 2026년 8월 3일부터 당신에게 옵니다
2026년 8월 3일부터 Google은 지불 거절의 비용을 개발자에게 넘깁니다. 구매 가격을 잃고 그 위에 은행의 지불 거절 수수료를 지불합니다. voidedReason이 7인 경우, 그것은 더 이상 잃어버린 매출일 뿐만 아니라 수수료가 붙은 비용 항목입니다. 지불 거절은 되돌릴 수 없으며 은행과의 사이에서 확정되지만, 그 이후의 출혈은 막을 수 있습니다. 무효화를 빠르게 포착하면 권한을 회수하고, 아직 제공 중인 것에 대해서는 환불받은 뒤 되돌려진 고객에게 드는 지출을 멈출 수 있습니다.
환불받은 고객에게 서비스를 계속 제공하는 비용을 계속 지불합니다
무효화가 나타나는 순간 구매 가격은 사라집니다. 당신이 여전히 통제하는 것은 제공을 계속하는 비용입니다. 환불받은 권한이 살아 있는 매 시간, 고객이 더 이상 자금을 대지 않는 것들에 대해 계속 지불합니다. 컴퓨팅, 모델 API 호출, 스토리지, 그리고 그들의 사용에 연결된 크리에이터나 파트너 지급입니다. 이 API로 구동되는 회수 시스템은 그 계량기를 끄는 방법입니다. 그것을 건너뛰면, 스토어가 이미 배상해 준 사람들을 위해 제품을 대는 셈입니다.
friendly fraud는 추세를 추적할 가치가 있는 패턴입니다
voidedReason이 5나 6은 일회성이 아닙니다. Fraud와 friendly fraud는 계정별, 기기별, 그리고 때로는 프로모션별로 뭉칩니다. API는 모든 무효화에 voidedSource와 voidedReason을 제공하므로, 각 반전을 고립된 비용으로 취급하는 대신 계정별로 남용 추세를 추적하기에 충분합니다. 두 번 지불 거절하는 고객은 첫 환불이 알려주지 않은 무언가를 당신에게 말하고 있습니다.
RefundHalt 방식으로 연결하기
모든 조각을 손에 쥐면 모델은 작습니다. 아무것도 하루를 통째로 기다리지 않도록 실시간으로 VoidedPurchaseNotification을 수신 대기하세요. 구독 갱신이 결코 혼동되지 않도록 orderId를 키로 삼아 진실의 원천으로서 Voided Purchases API를 호출하세요. 지불 거절이 후회 환불과 다르게 처리되도록 voidedSource와 voidedReason을 읽으세요. 30일 기간이 결코 물지 않을 만큼 촘촘한 일정으로 폴링하고, 접근을 끊는 것이 의도일 때는 항상 revoke 옵션을 켜고 환불하세요.
이것이 RefundHalt가 당신을 위해 돌리는 부분입니다. 실시간 알림을 소비하고, 모든 무효화를 API와 대조하며, 제품 전체가 아니라 정확한 주문을 회수하고, 비싼 것들이 묻히지 않고 표시되도록 은행 지불 거절을 일반 환불과 분리합니다. Pub/Sub 파이프라인과 폴링 작업을 직접 세우지 않고도, 몇 초 만에 접근이 회수되고 누가 무엇을 왜 무효화했는지의 기록을 얻습니다.
자주 묻는 질문
- 환불한 주문이 Voided Purchases API에 표시되지 않는 이유는 무엇인가요?
- 반환되는 것은 revoke된 주문뿐이기 때문입니다. 사용자 환불, 취소, 지불 거절, 그리고 Google이 시작한 환불은 항상 revoke되어 항상 나타납니다. 개발자가 시작한 환불은 당신이 revoke 옵션도 선택한 경우에만 나타납니다. revoke 없이 주문을 환불했다면, 그 주문은 정산되었지만 이 API에는 보이지 않습니다. 따라서 접근 권한을 회수하려는 의도가 있을 때는 항상 revoke를 켜고 환불하세요.
- Voided Purchases API는 얼마나 과거까지 거슬러 올라가나요?
- 30일입니다. startTime 매개변수는 현재 시각에서 30일을 뺀 값을 기본으로 하며 그보다 오래되게 설정할 수 없으므로, 이 엔드포인트는 아카이브가 아니라 한 달짜리 롤링 창입니다. 30일이 지나 만료된 무효화된 주문은 API에서 사라지며 되찾을 방법이 없습니다. 그래서 일정에 따라 폴링하고 실시간 알림으로 이를 보강합니다.
- 접근을 회수하려면 Real-time developer notifications와 Voided Purchases API 중 무엇을 사용해야 하나요?
- 둘 다 사용하세요. VoidedPurchaseNotification은 몇 초 안에 도착하여 보라고 알려주지만, Google 자체의 지침은 그것을 진실의 원천이 아니라 신호로 취급하는 것입니다. Voided Purchases API를 호출하여 현재 상태를 확인한 다음 회수하세요. 알림은 지연을 제거하고, API는 처리할 권위 있는 voidedSource와 voidedReason을 제공합니다.
- API에서 지불 거절과 일반 환불을 어떻게 구별하나요?
- voidedReason 필드를 읽으세요. 값이 7이면 고객의 은행이 결제를 되돌렸다는 의미의 지불 거절이고, 6은 friendly fraud입니다. 값이 1이면 후회 환불입니다. 이것이 중요한 이유는 2026년 8월 3일부터 Google이 지불 거절 구매 가격과 은행 수수료를 개발자에게 넘기므로, voidedReason이 7인 경우 일반 환불보다 비용이 더 많이 들기 때문입니다.
- Voided Purchases API는 구독을 포함하나요?
- 예. type 매개변수를 1로 설정하면 무효화된 인앱 구매와 무효화된 구독 구매를 모두 얻습니다. 기본값인 type 0은 인앱 상품 구매만 반환합니다. 구독의 경우, 하나의 purchaseToken이 모든 갱신을 포함하고 각 갱신 거래마다 새로운 orderId가 생성되므로, 정확한 무효화된 기간을 orderId로 식별하세요.
출처 및 추가 자료
- Google Play Developer API: Voided Purchases API guide
- Google Play Developer API: purchases.voidedpurchases.list method
- Google Play Developer API: purchases.voidedpurchases resource (voidedSource and voidedReason)
- Android Developers: Real-time developer notifications reference (VoidedPurchaseNotification)
- Android Developers: Fight fraud and abuse with Play Billing
RefundHalt
App Store와 Google Play 환불 자동 처리
계속 읽기
Apple이 판단을 내린 뒤 세 가지 App Store 환불 알림이 도착하고, REFUND_REVERSED는 판매를 되돌려준다
Apple은 App Store Server Notifications V2를 통해 네 가지 환불 메시지를 보내지만, 대부분의 앱은 두 가지만 처리합니다. REFUND는 권한을 회수하라고 알리고, REFUND_DECLINED는 판매를 그대로 유지하라는 뜻이며, REFUND_REVERSED는 판매를 되돌려주면서 당신이 회수한 것을 복원하라고 요청합니다. 각각 무엇이 필요한지 설명합니다.
이제 모든 Apple 환불 요청에는 이유가 함께 오며, consumptionRequestReason 이 그것을 읽는 방법입니다
WWDC24 이후 모든 Apple CONSUMPTION_REQUEST 는 consumptionRequestReason, 즉 고객이 직접 밝힌 환불 사유를 담고 있습니다. 값은 UNINTENDED_PURCHASE 부터 LEGAL 까지 다섯 가지이며, 각각은 12시간의 대응 창 안에서 여러분이 돌려보내는 내용을 바꿔야 합니다. 여기서 그 하나하나를 읽는 방법을 설명합니다.