Скасування доступу після повернення коштів: крок, який Apple і Google не роблять за вас
І Apple, і Google можуть оформити повернення коштів так, що покупка залишиться в клієнта. Тут точно пояснено, коли доступ скасовується автоматично, коли це має зробити ваш сервер, і яке саме сповіщення майже ніхто не обробляє.

Головне
- У Google Play повернення коштів за замовлення саме по собі не скасовує доступ користувача. Доступ скасовується лише тоді, коли в Play Console позначено "Remove entitlement", або коли виклик API передає revoke=true, а це не значення за замовчуванням.
- Повернення коштів, оформлене без опції revoke, взагалі ніколи не з'являється у Voided Purchases API. Завдання зі скасування доступу, яке читає лише цей фід, непомітно пропустить кожне таке замовлення.
- Apple може автоматично скасувати ліцензію для non-consumable товарів і підписок, але витратний товар вважається вже доставленим у момент, коли його витрачено, тому в StoreKit немає механізму, щоб revoke його. Ваш сервер має самостійно списати баланс.
- Apple також може скасувати власне повернення коштів. Сповіщення REFUND_REVERSED означає, що раніше надане повернення коштів було скасовано, revocationDate повертається до null, і будь-який доступ чи баланс, які ви прибрали, потрібно відновити.
- Поле voidedReason у Google містить дев'ять значень, від remorse і accidental_purchase до fraud, friendly_fraud і unacknowledged_purchase, тож той самий фід, що каже вам revoke доступ, також показує, які скасування є шахрайством, а які є вашою власною помилкою інтеграції.
- Кожен день, коли повернена покупка зберігає доступ, це день, за який ви платите за обчислення, зберігання даних і виклики API позаду неї, на гроші, які вже повернули.
Схвалення повернення коштів і видалення доступу, це два окремі кроки, і автоматично відбувається лише один із них. Розробники, які вважають, що оброблене повернення коштів дорівнює скасованій покупці, помиляються на обох великих майданчиках, по-різному і з різних причин. Щоб revoke доступ після повернення коштів, а не виявити через кілька місяців, що цього так і не сталося, потрібно точно знати, де закінчується автоматизація.
Чому повернення коштів не revoke доступ за замовчуванням
І Apple, і Google трактують гроші та доступ як дві різні операції. Грошова частина, це політика магазину: щойно повернення коштів схвалено, воно остаточне, і облік оновлюється. Частина з доступом є опційною, або обов'язком розробника, або і тим, і іншим, залежно від платформи та типу покупки. Жоден магазин не припускає, що ви хочете, щоб доступ зник тієї ж миті, коли пройшло повернення коштів, адже багато законних випадків (часткове повернення коштів, кредит доброї волі, жест підтримки) взагалі не мають забирати продукт.
Google Play: прапорець, якого ніхто не помічає
У Play Console повернення коштів за замовлення, це форма з двох частин: спершу обираєте суму повернення, а потім окремо вирішуєте, чи позначати "Remove entitlement". Залишити прапорець непозначеним, це типовий шлях для більшості агентів підтримки, бо більшість запитів на повернення коштів є жестами доброї волі, коли клієнт залишає собі те, що купив. Проблема в тому, що на екрані підтвердження повернення коштів ніщо не попереджає, що так само клієнт залишає собі підписку чи товар усередині застосунку після повернення коштів, яке мало бути остаточним.
Шлях через API має ту саму пастку
Такий самий вибір існує і в коді. Ендпоінт orders.refund приймає необов'язковий параметр запиту revoke; коли він true, доступ до товару припиняється негайно, а для підписки з повторюваними платежами разом із цим скасовується кожен майбутній платіж. Якщо параметр опустити, він за замовчуванням дорівнює false: повернення коштів проходить, запис про покупку оновлюється, а користувач продовжує користуватися продуктом точно так само, як і раніше.
| Дія | Доступ скасовано? | З'являється у Voided Purchases API? |
|---|---|---|
| Повернення коштів через Play Console, "Remove entitlement" не позначено | Ні | Ні |
| Повернення коштів через Play Console, "Remove entitlement" позначено | Так, негайно | Так |
| orders.refund API, параметр revoke пропущено (за замовчуванням false) | Ні | Ні |
| orders.refund API, revoke=true | Так, негайно | Так |
Саме останній рядок і підловлює інтеграції. Voided Purchases API існує для того, щоб ви могли побудувати конвеєр скасування доступу без ручного опитування Play Console, але він показує лише замовлення, які справді було анульовано зі скасуванням доступу. Повернення коштів, оброблене без прапорця, або без revoke=true, невидиме для цього фіду за задумом, а не через баг.

Читання поля voidedReason Коли покупка справді з'являється як voided, поле voidedReason повідомляє чому, за шкалою з дев'яти значень: other, remorse, not_received, defective, accidental_purchase, fraud, friendly_fraud, chargeback і unacknowledged_purchase. Це не просто облік. Remorse і accidental_purchase, це звичайні повернення коштів. Fraud, friendly_fraud і chargeback варто скеровувати в чергу перевірки на шахрайство, а не просто revoke і забути, бо той самий обліковий запис статистично з більшою ймовірністю повторить це знову.
Одна операційна деталь має значення, якщо ви будуєте систему прямо на основі цього фіду: purchases.voidedpurchases.list обмежено 6000 запитів на день і 30 запитів у будь-якому 30-секундному вікні. Дизайн, який опитує систему в щільному циклі, швидко впирається у цю стелю; передбачений патерн, це планове опитування з токенами пагінації, а не безперервний polling.
Apple: App Store може revoke ліцензію, але не витратний товар
App Store Server Notifications V2 від Apple надсилає сповіщення REFUND у момент, коли повернення коштів проходить, однаково для витратних товарів, невитратних товарів, автовідновлюваних підписок і підписок без автоматичного поновлення. Те, що відбувається після цього сповіщення, повністю залежить від типу покупки, бо власний механізм скасування доступу в StoreKit охоплює лише два з чотирьох типів.
Чому витратні товари є винятком
Невитратний товар або підписка мають постійне право доступу, яке системи Apple можуть вимкнути. У витратного товару такого немає: щойно він використаний, він вважається доставленим, і в StoreKit більше нічого не залишається revoke. Якщо клієнт купив 500 монет, витратив їх і отримав повернення коштів, з погляду Apple монети вже зникли. Єдиний облік, де вони ще є, це ваш. Сповіщення REFUND, це ваш єдиний сигнал знайти відповідний original transaction ID і списати цей баланс зі своїх власних записів; на боці Apple ніхто не зробить цього за вас.
| Сповіщення | Що це означає | Що має зробити ваш сервер |
|---|---|---|
| REFUND | Apple повернула кошти за витратний товар, невитратний товар, автовідновлювану чи безповторну покупку | Негайно revoke доступ до non-consumable товару та підписки; самостійно списати баланс витратного товару |
| REFUND_DECLINED | Apple відхилила запит клієнта на повернення коштів | Нічого. Доступ ніколи не був під загрозою |
| REFUND_REVERSED | Apple скасувала повернення коштів, яке вже надала | Відновити будь-який доступ чи баланс, які довелося прибрати через сповіщення REFUND |
| CONSUMPTION_REQUEST | Apple хоче отримати ваші докази перед тим, як вирішити щодо повернення коштів | Відповісти протягом 12 годин; це спрацьовує перед REFUND, а не після нього |
Сповіщення, яке майже ніхто не обробляє
REFUND_REVERSED, це те сповіщення, яке більшість інтеграцій пропускають повністю, бо воно описує ситуацію, коли Apple скасовує власне рішення, а це випадок, до якого мало хто з розробників готується із самого початку. Коли воно надходить, revocationDate у транзакції повертається до null: наскільки це стосується Apple, покупка знову дійсна. Якщо ваш сервер видалив право доступу, скасував запис підписки або обнулив баланс витратного товару, коли надійшов початковий REFUND, то REFUND_REVERSED, це сигнал повернути все назад.
Як закрити цю прогалину, крок за кроком
- У Google Play зробіть revoke=true типовим значенням для кожного шляху повернення коштів, чи то через Play Console, чи через API, якщо тільки конкретний випадок підтримки не вимагає, щоб клієнт зберіг доступ.
- Опитуйте purchases.voidedpurchases.list за розкладом із токенами пагінації, а не в щільному циклі, і тримайтеся в межах менше ніж 6000 запитів на день і 30 за 30 секунд.
- Скеровуйте значення voidedReason на кшталт fraud, friendly_fraud і chargeback у чергу перевірки, замість автоматичного revoke із закриттям справи.
- На боці Apple підпишіться на App Store Server Notifications V2 і revoke доступ до non-consumable товару та підписки в момент, коли сповіщення REFUND називає їх.
- Зберігайте original transaction ID для кожної покупки витратного товару, щоб сповіщення REFUND могло знайти точний баланс для списання без ручного пошуку.
- Явно обробляйте REFUND_REVERSED і фіксуйте кожне скасування доступу з прив'язкою до його transaction ID або order ID, щоб скасування могло точно відновити те, що було прибрано.
Скільки насправді коштує ця прогалина
Усе це не про справедливість щодо клієнта; це про те, що продовжує працювати після того, як гроші вже пішли. Повернена підписка, яка продовжує викликати ваш API, зберігати дані та споживати обчислювальні ресурси, це не моральна проблема, це рядок у витратах: ви оплачуєте рахунок за інфраструктуру для покупки, за яку вже повернули дохід. Магазини не відстежують цю вартість за вас, бо з їхнього боку повернення коштів завершене в ту мить, коли рухаються гроші. Частина з доступом повністю на вашому рахунку і під вашим контролем.
Це та сама причина, чому RefundHalt трактує скасування доступу як частину конвеєра повернення коштів, а не як щось другорядне: подія REFUND і зміна доступу фіксуються як одна дія, а подія REFUND_REVERSED автоматично відновлює стан, замість того щоб чекати, поки хтось помітить, що клієнт досі не має доступу до того, що Apple вже повернула.
Поширені запитання
- Чи скасовує повернення коштів на Google Play доступ автоматично?
- Ні. Доступ скасовується лише якщо в Play Console позначено "Remove entitlement", або якщо виклик orders.refund API включає revoke=true. Жодне з цього не є значенням за замовчуванням, тож повернення коштів може пройти, а клієнт продовжить користуватися продуктом.
- Що відбувається, коли на App Store повертають кошти за витратний товар усередині застосунку?
- Apple надсилає сповіщення REFUND, але в StoreKit немає способу revoke витратний товар, бо він вважається доставленим, щойно його витрачено. Сервер розробника має самостійно знайти оригінальну транзакцію і вручну списати баланс.
- Що таке сповіщення REFUND_REVERSED і чи потребує воно особливої обробки?
- REFUND_REVERSED означає, що Apple скасувала повернення коштів, яке вже надала; revocationDate повертається до null, і покупка знову дійсна. Будь-який доступ чи баланс, які прибрав початковий REFUND, потрібно відновити, а більшість інтеграцій узагалі ніколи не слухають це сповіщення.
- Чому повернене замовлення Google Play може не з'явитися у Voided Purchases API?
- API показує лише замовлення, які було анульовано зі скасуванням доступу. Якщо повернення коштів обробили без опції revoke, замовлення взагалі не повертається цим ендпоінтом, тож конвеєр, який покладається лише на нього, повністю пропустить ці повернення коштів.
- Як часто я можу опитувати Voided Purchases API від Google?
- Ендпоінт обмежено до 6000 запитів на день і 30 запитів у будь-якому 30-секундному вікні, тож він призначений для планового опитування з токенами пагінації, а не для безперервного polling.
Джерела та додаткове читання
- Apple: Handling refund notifications
- Apple: notificationType (App Store Server Notifications)
- Apple: revocationReason (App Store Server Notifications)
- Google: Voided Purchases API overview
- Google: purchases.voidedpurchases REST resource
- Google: orders.refund API reference (revoke parameter)
- Google Play Console Help: manage your app's orders and issue refunds
RefundHalt
Автопілот повернень для App Store і Google Play
Читайте далі
The refunds you cannot contest: what Apple and Google decide without asking you
Only two refund flows ever ask for your evidence: Apple's consumption request and Google Play's chargeback review. Everything else, the 48-hour Play refund, support refunds, voided purchases, unacknowledged trials, is decided without you. Here is the full map, what you can prevent, and what you can only record.
Apple's CONSUMPTION_REQUEST, explained: the 12 hours that decide your refund
When a customer asks Apple for a refund, Apple asks you first. Here's exactly what the CONSUMPTION_REQUEST notification contains, what Apple expects back, how to answer one end to end, and why most developers silently forfeit.