Google Play에서 발생하는 환불은 조용히 처리됩니다. 결제가 취소되고 고객은 계속 앱을 사용하지만, 직접 확인하지 않는 한 여러분 쪽에서는 아무것도 달라지지 않습니다. 바로 이럴 때 확인하라고 있는 것이 Voided Purchases API입니다. 이 API는 취소, 환불, 차지백된 주문을 반환하므로, 고객이 더 이상 비용을 지불하지 않는 항목에 대한 액세스를 회수할 수 있습니다. 예약된 작업을 연결해 응답을 읽고 권한을 취소하면 그것으로 전체 메커니즘이 끝입니다.
거의 모든 팀이 걸려 넘어지는 한 가지가 있는데, 이는 코드가 아니라 정책의 문제입니다. 이 API는 실제로 취소(revoke)된 주문만 나열합니다. Play Console에서 revoke 옵션을 체크하지 않고 구매를 환불하면 그 주문은 여기에 전혀 나타나지 않으므로, 여러분의 작업은 완벽하게 깨끗하게 실행되는 반면 환불받은 고객은 판매한 모든 것을 그대로 손에 쥔 채 떠나게 됩니다. 이어지는 내용에서는 이 API를 필드별로 살펴보고, API를 제약하는 한도, 그리고 연결해두지 않았을 때 조용히 새어나가는 비용까지 다룹니다.
핵심 요약
purchases.voidedpurchases.list 메서드로 호출하는 Voided Purchases API는 Google Play가 취소, 환불, 또는 차지백 처리한 주문을 나열합니다. 이를 통해 고객이 더 이상 보유하지 않는 구매 항목의 액세스를 박탈하는 시스템을 구축할 수 있습니다.
이 API는 취소(revoke)된 주문만 보여줍니다. revoke 없이 발행된 개발자 환불은 여기서 보이지 않으므로, 액세스를 회수하려면 revoke를 켠 상태로 환불해야 합니다.
조회 가능 범위는 이동하는(rolling) 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회입니다. 따라서 각 요청을 특정 시간 범위로 제한하고 continuation 토큰으로 페이지를 넘기는 방식을 사용해야 하며, 주문 하나당 요청 하나씩 보내는 방식은 피해야 합니다.
API가 반환하는 정보
이 엔드포인트는 단 하나의 질문에 답합니다. 바로 이 앱의 어떤 주문이 최근에 취소(void)되었는가입니다. 취소(void)는 취소, 환불, 차지백이라는 세 가지 결과를 아우르며, 이들 모두 고객에게 돈을 돌려주는 결과로 이어집니다. 이 API는 일회성 인앱 상품과 구독 모두를 다루며, 파라미터 하나로 범위를 지정합니다. type을 기본값인 0으로 두면 취소된 인앱 상품 구매만 반환됩니다. type을 1로 설정하면 취소된 인앱 구매와 취소된 구독 구매를 함께 받게 됩니다.
응답의 각 항목은 짧지만 중요한 필드 집합을 가진 voided purchase 객체입니다.
orderId는 일회성 구매, 구독, 또는 구독 내 개별 갱신 하나하나를 고유하게 식별합니다. 이를 조인 키(join key)로 사용하세요.
purchaseToken은 일회성 구매나 구독을 식별하지만 갱신을 구분하지는 못하므로, 갱신 구분에는 orderId를 사용하세요.
purchaseTimeMillis는 구매가 발생한 시점을 에포크(epoch) 이후 밀리초 단위로 기록합니다.
voidedTimeMillis는 취소, 환불, 또는 차지백이 발생한 시점을 마찬가지로 에포크 이후 밀리초 단위로 기록합니다.
voidedSource는 누가 취소를 시작했는지 나타내며, 0은 사용자, 1은 개발자, 2는 Google입니다.
voidedReason은 원인을 0부터 8까지의 정수로 나타냅니다.
voidedQuantity는 수량 기반 부분 환불(quantity-based partial refund)에서 취소된 수량을 담고 있으며, includeQuantityBasedPartialRefund가 true일 때만 나타납니다.
하나의 purchaseToken이 구독 전체를 포괄하는 반면 갱신할 때마다 새로운 orderId가 발급되므로, 토큰을 기준으로 권한(entitlement)을 관리하면 잘못된 기간을 취소하게 됩니다. 대신 orderId를 기준으로 삼으세요.
무언가를 처리하기 전에 voidedReason부터 확인하세요
voidedReason은 단순한 목록을 실제 의사결정으로 바꿔주는 필드입니다. 단순 변심 환불과 은행 차지백은 같은 피드에 나란히 등장하지만 전혀 다른 사안이기 때문입니다. 전체 목록은 다음과 같습니다.
1. Other은 별도로 지정된 범주가 없습니다. 취소 처리 후 넘어가면 됩니다.
2. Remorse는 마음이 바뀐 고객으로, 흔히 있는 환불입니다.
3. Not_received는 상품이 도착하지 않았다는 주장으로, 여러분의 전달(delivery) 과정을 점검해볼 가치가 있습니다.
4. Defective는 제품이 작동하지 않았다는 의미로, 기록해두어야 할 품질 신호입니다.
5. Accidental_purchase는 의도하지 않은 구매로, 공유 기기에서 자주 발생합니다.
6. Fraud는 Google이 사기로 표시한 거래입니다.
7. Friendly_fraud는 실제 카드 소유자가 자신이 실제로 결제한 내역에 이의를 제기하는 차지백입니다.
8. Chargeback은 고객의 은행이 결제를 되돌리는 것으로, 은행 측에서는 최종 확정이며 이제 그 비용이 여러분에게 청구됩니다.
9. Unacknowledged_purchase는 여러분의 앱이 확인(acknowledge) 처리하지 않은 구매를 Google이 자동으로 환불하는 경우입니다.
사유 9는 스스로 자초한 환불입니다. 앱이 확인 처리를 하지 않은 구매는 Google에 의해 자동으로 환불되므로, 이 피드에 등장하는 unacknowledged_purchase는 구매자의 선택이 아니라 놓친 호출 하나 때문에 포기한 매출입니다.
목록을 조용히 비워내는 30일 창
이 API는 과거 30일까지만 조회할 수 있습니다. startTime의 기본값은 현재 시점에서 30일을 뺀 값이며 그보다 이전으로 설정할 수 없고, endTime의 기본값은 현재입니다. 즉, 여러분이 얻는 것은 영구 보관소가 아니라 이동하는 한 달짜리 창입니다.
이 사실이 의미하는 바는 명확합니다. 폴링 작업이 5주 동안 고장 난 채 방치되면, 첫 주의 취소 내역은 이미 API에서 사라진 뒤라 어떤 호출로도 다시 가져올 수 없습니다. 그 주문들은 취소 처리되지 않은 채 남고, 다른 곳에 기록해두지 않았다면 그런 일이 있었는지조차 알 수 없습니다. 그물에는 여러분의 가장 긴 장애 기간만큼 정확히 큰 구멍이 뚫려 있는 셈입니다. 최소 하루 한 번은 폴링하고 실시간 알림과 대조해, 30일을 실제로 그것이 의미하는 대로, 즉 확실한 삭제 시한으로 취급하세요.
주문이 아예 나타나는지 여부는 revoke에 달려 있습니다
팀들이 이 API가 고장 났다고 말하는 가장 큰 이유가 바로 이것입니다. 이 API는 취소(revoke)된 주문만 반환할 뿐 그 외에는 아무것도 반환하지 않습니다. 사용자가 시작한 환불, 취소, 차지백, Google이 시작한 환불은 모두 자동으로 revoke 처리되므로 항상 피드에 나타납니다. 예외는 개발자가 시작한 환불입니다. Play Console에서든 Orders API를 통해서든 직접 환불을 발행하면, revoke 여부는 별도로 내려야 하는 결정으로 남습니다. 이를 선택하지 않으면 주문은 고객과의 관계에서는 정산되지만 이 피드에는 결코 나타나지 않습니다.
결론은 간단합니다. 액세스를 차단하는 것이 목표라면 revoke를 활성화한 채로 환불하세요. 이를 생략하면 돈은 돌려줬지만 문은 열어둔 셈이 되고, 아무리 정교하게 만든 취소(revocation) 작업이라도 처리할 대상 자체가 없게 됩니다.
할당량을 넘기지 않고 폴링하기
이 엔드포인트에는 속도 제한(rate limit)이 있으며, 그 한도는 부주의한 루프가 쉽게 걸릴 만큼 낮게 설정되어 있습니다. 하루 6,000회 쿼리(태평양 표준시 기준)가 허용되며, 30초 구간 안에서는 절대 30회를 넘을 수 없습니다. 이 예산은 시간 창(window) 단위 폴링에는 적합하지만, 주문 하나당 호출 하나를 사용하는 설계에는 불리합니다.
시간 창과 continuation 토큰
maxResults의 기본값은 1,000이며, 이는 동시에 설정 가능한 최댓값이기도 합니다. 한 창(window)에 한 페이지가 담을 수 있는 것보다 많은 취소 내역이 있다면, 응답에는 nextPageToken을 담은 tokenPagination 객체가 함께 옵니다. 다음 호출에서 이 토큰을 전달해 다음 페이지로 넘어가세요. startTime과 endTime으로 창의 경계를 고정하고, 토큰이 더 이상 나오지 않을 때까지 페이지를 넘긴 다음에야 창을 이동시키세요. 이런 방식이라면 30초 버스트 한도와 일일 허용량 모두를 넘지 않을 수 있습니다.
실시간 알림이 하루 단위 공백을 메워줍니다
하루 한 번 폴링하더라도 최대 24시간 동안은 아무것도 파악하지 못하는 상태가 되며, 긴 공백이야말로 30일 창이 가차 없이 벌하는 대상입니다. 실시간 개발자 알림은 바로 이 지연을 메워줍니다. 구매가 취소되는 즉시 VoidedPurchaseNotification이 여러분이 관리하는 Cloud Pub/Sub 토픽에 게시되고, 백엔드는 몇 초 안에 이를 수신해 처리합니다. 페이로드는 다음과 같이 간결합니다.
purchaseToken은 원래 구매의 토큰입니다.
orderId는 취소된 거래의 ID로, 구독이 갱신될 때마다 새로 발급됩니다.
productType은 구독이면 1, 일회성 구매면 2입니다.
refundType은 전액 환불(1)과 수량 기반 부분 환불(2)을 구분합니다.
이 알림은 절대적인 진실이 아니라 하나의 경고로 받아들이세요. VoidedPurchaseNotification이 도착하면 Voided Purchases API를 조회해 주문이 실제로 어떤 상태인지 확인한 다음에야 취소 처리를 진행하세요. 알림은 확인하라고 알려줄 뿐이고, 진실을 말해주는 것은 API입니다.
비용으로 따지면 얼마나 드는가
이 API는 배관에 불과하지만, 그 배관을 놓는 이유는 청구서 때문이며, 그 청구서 위의 두 숫자가 점점 커지고 있습니다.
2026년 8월 3일부터 차지백 비용은 여러분의 몫입니다
2026년 8월 3일부터 Google은 차지백 비용을 개발자에게 전가합니다. 구매 대금을 잃는 것은 물론, 그 위에 은행의 차지백 수수료까지 부담하게 됩니다. voidedReason이 7이면 이는 더 이상 단순한 매출 손실이 아니라 수수료가 붙은 청구 항목이 됩니다. 차지백 자체는 은행 측에서 최종 확정된 사안이라 되돌릴 수 없지만, 취소를 빠르게 포착하면 권한을 회수할 수 있고, 아직 전달 중인 서비스가 있다면 환불받은 뒤 차지백까지 발생한 고객에게 계속 비용을 지출하는 일을 멈출 수 있습니다.
이미 환불받은 고객에게 계속 비용을 대주게 됩니다
판매 대금은 취소(void)가 발생하는 순간 사라집니다. 여러분이 여전히 통제할 수 있는 것은 전달을 계속하는 데 드는 비용입니다. 환불된 권한이 활성 상태로 남아 있는 매 시간마다, 고객이 더 이상 부담하지 않게 된 비용, 즉 컴퓨팅, 모델 제공업체 호출, 스토리지, 그리고 그 활동이 유발하는 크리에이터나 파트너 정산금까지 계속해서 여러분 쪽으로 청구됩니다. 이 API를 기반으로 취소(revocation) 파이프라인을 구축하는 것이 바로 그 지출을 차단하는 방법입니다. 이를 생략하면 스토어가 이미 환불해준 사람들을 위해 계속 제품 비용을 대신 지불하는 셈이 됩니다.
Friendly fraud는 하나의 사건이 아니라 추세입니다
voidedReason이 5나 6인 경우는 좀처럼 단독으로 발생하지 않습니다. Fraud와 friendly fraud는 특정 계정, 기기, 때로는 특정 프로모션을 중심으로 모여서 나타납니다. 이 API가 모든 취소 건에 voidedSource와 voidedReason을 함께 붙여주므로, 각각의 취소를 개별적인 손실로 받아들이는 대신 계정별 악용 추세를 파악할 수 있는 충분한 정보를 갖추게 됩니다. 두 번째로 차지백을 발생시키는 계정은 첫 번째 환불이 말해주지 않았던 것을 알려주고 있는 것입니다.
종합해 보면
필요한 요소들만 갖추면 전체 모델은 그리 크지 않습니다. VoidedPurchaseNotification을 실시간으로 수신해 아무것도 하루 종일 대기하지 않도록 하세요. Voided Purchases API를 진실의 원천으로 삼되, orderId를 기준으로 삼아 갱신 건들이 서로 뒤섞이지 않도록 하세요. voidedSource와 voidedReason을 읽어 차지백을 단순 변심 환불과 별도로 처리하세요. 30일 창이 결코 발목을 잡지 않을 만큼 촘촘한 주기로 폴링하고, 액세스를 차단할 의도가 있을 때는 언제나 revoke를 활성화한 채로 환불하세요.
바로 이 계층을 Refund Sensor가 여러분을 대신해 처리합니다. 실시간 알림을 수신하고, 모든 취소 건을 API와 대조해 확인하며, 상품 전체가 아니라 정확히 해당 주문만 취소 처리하고, 은행 차지백을 일반 환불과 분리해 관리함으로써 비용이 큰 건들이 숨지 않고 드러나도록 합니다. 여러분은 몇 초 안에 액세스가 회수되는 것과, 누가 무엇을 왜 취소했는지에 대한 전체 기록을 얻게 되며, Pub/Sub 파이프라인이나 폴링 작업을 직접 구축할 필요가 없습니다.
이 규칙들이 문서화된 곳
자주 묻는 질문
이 피드에는 취소(revoke)된 주문만 담기기 때문입니다. 사용자가 시작한 환불, 취소, 차지백, Google이 시작한 환불은 모두 자동으로 revoke 처리되어 항상 나타납니다. 반면 직접 발행한 환불은 revoke도 함께 선택한 경우에만 나타납니다. revoke 없이 환불하면 주문은 정산되지만 여기서는 보이지 않으므로, 액세스를 회수하는 것이 목표라면 항상 revoke를 켜세요.
최대 30일까지이며, 그 이상은 불가능합니다. startTime의 기본값은 현재 시점에서 30일을 뺀 값이며 그보다 이전 값은 허용되지 않으므로, 이 엔드포인트는 영구 보관소가 아니라 이동하는 한 달짜리 창처럼 동작합니다. 취소 건이 30일을 넘기면 되돌릴 방법 없이 사라지므로, 바로 이 때문에 일정에 따라 폴링하고 실시간 알림으로 이를 뒷받침해야 합니다.
둘 다 각자의 역할이 있습니다. VoidedPurchaseNotification은 몇 초 안에 도착해 확인하라고 알려주지만, Google의 안내에 따르면 이를 확정된 사실이 아니라 신호로 취급해야 합니다. Voided Purchases API로 현재 상태를 확인한 다음 revoke하세요. 알림은 지연을 없애주고, API는 실제로 조치를 취할 근거가 되는 확정된 voidedSource와 voidedReason을 제공합니다.
voidedReason을 확인하세요. 7은 고객의 은행이 결제를 되돌린 차지백이고, 6은 friendly fraud입니다. 1은 일반적인 단순 변심 환불입니다. 이 구분이 중요한 이유는 2026년 8월 3일부터 Google이 차지백 대금과 은행 수수료를 개발자에게 전가하기 때문이며, 그래서 7은 일반 환불보다 더 많은 비용이 듭니다.
포함됩니다. type 파라미터를 1로 설정하면 취소된 인앱 구매와 취소된 구독 구매를 함께 받을 수 있으며, 기본값인 0은 인앱 상품만 반환합니다. 구독의 경우, 하나의 purchaseToken이 모든 갱신에 걸쳐 유지되는 반면 각 갱신 거래는 고유한 orderId를 가지므로, 정확한 취소 기간은 orderId로 파악하세요.






