Отзыв доступа после возврата средств: шаг, который Apple и Google не делают за вас
И Apple, и Google могут оформить возврат средств, пока клиент сохраняет доступ к покупке. Здесь точно объясняется, когда доступ отзывается автоматически, когда это должен делать ваш сервер, и какое уведомление почти никто не обрабатывает.

Главное
- В Google Play возврат средств по заказу сам по себе не отменяет доступ пользователя. Доступ отзывается только если в Play Console отмечена опция "Remove entitlement" или если вызов API передаёт revoke=true, а это не поведение по умолчанию.
- Возврат средств, оформленный без опции отзыва, вообще никогда не появится в Voided Purchases API. Процесс отзыва доступа, который читает только этот источник данных, незаметно пропустит все такие заказы.
- Apple может автоматически отозвать лицензию для неисчезающих покупок и подписок, но расходуемая покупка считается доставленной в момент использования, поэтому у StoreKit нет механизма для её отзыва. Вашему серверу придётся самому списывать баланс.
- Apple также может отменить собственный возврат средств. Уведомление REFUND_REVERSED означает, что ранее одобренный возврат был отменён, revocationDate снова становится null, а любой доступ или баланс, которые вы убрали, нужно восстановить.
- Поле voidedReason у Google принимает девять значений, от remorse и accidental_purchase до fraud, friendly_fraud и unacknowledged_purchase, поэтому один и тот же источник данных, который говорит вам, когда отзывать доступ, также показывает, какие отзывы связаны с мошенничеством, а какие с ошибкой в вашей собственной интеграции.
- Каждый день, в течение которого возвращённая покупка сохраняет доступ, это день, за который вы платите за вычисления, хранение и вызовы API, стоящие за ней, деньгами, которые вы уже вернули.
Одобрение возврата средств и отзыв доступа, это два отдельных шага, и только один из них происходит автоматически. Разработчики, которые считают, что обработанный возврат равнозначен отозванной покупке, ошибаются в обоих крупных магазинах, по-разному и по разным причинам. Чтобы отзывать доступ после возврата средств, а не обнаружить месяцы спустя, что этого никогда не происходило, нужно точно знать, где заканчивается автоматизация.
Почему возврат средств не отзывает доступ по умолчанию
И 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" отмечен | Да, немедленно | Да |
| API orders.refund, параметр revoke не указан (по умолчанию false) | Нет | Нет |
| API orders.refund, revoke=true | Да, немедленно | Да |
Именно последняя строка ловит интеграции врасплох. Voided Purchases API существует, чтобы вы могли построить конвейер отзыва доступа без ручной проверки Play Console, но он перечисляет только те заказы, которые были действительно аннулированы с отзывом. Возврат средств, обработанный без отмеченного флажка или без revoke=true, невидим для этого источника данных по замыслу, а не из-за ошибки.

Чтение поля voidedReason Когда покупка действительно отображается как аннулированная, поле voidedReason сообщает причину по шкале из девяти значений: other, remorse, not_received, defective, accidental_purchase, fraud, friendly_fraud, chargeback и unacknowledged_purchase. Это не просто учёт. Remorse и accidental_purchase, это обычные возвраты. Fraud, friendly_fraud и chargeback стоит направлять в очередь проверки на мошенничество, а не по простому пути отзыва и забвения, потому что тот же аккаунт статистически с большей вероятностью повторит это снова.
Один операционный нюанс важен, если вы работаете с этим источником данных напрямую: purchases.voidedpurchases.list ограничен 6000 запросов в день и 30 запросами в любое окно из 30 секунд. Дизайн, который опрашивает в плотном цикле, быстро упирается в этот потолок; предполагаемый паттерн, это запланированное получение данных с токенами пагинации, а не непрерывный опрос.
Apple: App Store может отозвать лицензию, но не расходуемую покупку
App Store Server Notifications V2 от Apple отправляют уведомление REFUND в момент завершения возврата средств, одинаково для расходуемых, нерасходуемых покупок, автоматически продлеваемых подписок и подписок без автопродления. То, что происходит после этого уведомления, целиком зависит от типа покупки, потому что собственный механизм отзыва StoreKit покрывает только два типа из четырёх.
Почему расходуемые покупки, это исключение
Нерасходуемая покупка или подписка имеет постоянное право доступа, которое системы Apple могут отключить. У расходуемой покупки этого нет: как только она использована, она считается доставленной, и в StoreKit больше нечего отзывать. Если клиент купил 500 монет, потратил их и получил возврат средств, монеты уже исчезли с точки зрения Apple. Единственный реестр, где они ещё числятся, это ваш. Уведомление REFUND, это ваш единственный сигнал найти соответствующий исходный идентификатор транзакции и списать этот баланс из своих собственных записей; ничего на стороне Apple не сделает это за вас.
| Уведомление | Что это значит | Что должен сделать ваш сервер |
|---|---|---|
| REFUND | Apple вернула средства за расходуемую, нерасходуемую, автопродлеваемую или неавтопродлеваемую покупку | Немедленно отозвать доступ к нерасходуемым покупкам и подпискам; самостоятельно списать баланс расходуемой покупки |
| 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 в очередь проверки, а не по пути автоматического отзыва и закрытия.
- Для Apple подпишитесь на App Store Server Notifications V2 и отзывайте доступ к нерасходуемым покупкам и подпискам в момент, когда уведомление REFUND называет их.
- Храните исходный идентификатор транзакции для каждой расходуемой покупки, чтобы уведомление REFUND могло найти точный баланс для списания без ручного поиска.
- Обрабатывайте REFUND_REVERSED явно и фиксируйте каждый отзыв доступа привязанным к его идентификатору транзакции или заказа, чтобы отмена могла точно восстановить то, что было убрано.
Во что на самом деле обходится этот пробел
Всё это не о справедливости по отношению к клиенту; это о том, что продолжает работать после того, как деньги уже ушли. Возвращённая подписка, которая продолжает обращаться к вашему API, хранить данные и потреблять вычислительные ресурсы, это не моральная проблема, это статья расходов: вы платите по счёту за инфраструктуру для покупки, доход от которой вы уже вернули. Магазины не отслеживают эти расходы за вас, потому что с их стороны возврат завершён в момент, когда деньги переместились. Сторона доступа целиком на вашей ответственности.
Именно поэтому RefundHalt рассматривает отзыв доступа как часть конвейера возврата средств, а не как нечто второстепенное: событие REFUND и изменение доступа фиксируются как единое действие, а событие REFUND_REVERSED автоматически восстанавливает состояние, вместо того чтобы ждать, пока кто-то заметит, что клиент всё ещё заблокирован от того, что Apple ему уже вернула.
Частые вопросы
- Отзывает ли возврат средств за покупку в Google Play доступ автоматически?
- Нет. Доступ отзывается только если в Play Console отмечена опция "Remove entitlement" или если вызов API orders.refund включает revoke=true. Ни один из этих вариантов не является поведением по умолчанию, поэтому возврат может быть оформлен, пока клиент продолжает пользоваться продуктом.
- Что происходит, когда расходуемая внутриигровая покупка возвращается в App Store?
- Apple отправляет уведомление REFUND, но у StoreKit нет способа отозвать расходуемую покупку, потому что она считается доставленной сразу после использования. Сервер разработчика должен найти исходную транзакцию и списать баланс вручную.
- Что такое уведомление REFUND_REVERSED и нужна ли для него особая обработка?
- REFUND_REVERSED означает, что Apple отменила уже одобренный возврат средств; revocationDate снова становится null, и покупка вновь считается действительной. Любой доступ или баланс, убранные из-за исходного REFUND, нужно восстановить, а большинство интеграций вообще никогда не слушают это уведомление.
- Почему возвращённый заказ Google Play может не появиться в Voided Purchases API?
- API перечисляет только заказы, аннулированные с отзывом доступа. Если возврат средств был обработан без опции отзыва, этот заказ вообще не возвращается данным эндпоинтом, поэтому конвейер, полагающийся только на него, полностью пропустит такие возвраты.
- Как часто можно опрашивать Voided Purchases API от Google?
- Эндпоинт ограничен 6000 запросов в день и 30 запросами в любое окно из 30 секунд, поэтому он предназначен для запланированного получения данных с токенами пагинации, а не для непрерывного опроса.
Источники и дополнительное чтение
- 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.