Три уведомления App Store о возврате приходят после решения Apple, а REFUND_REVERSED возвращает продажу вам
Apple отправляет четыре сообщения о возврате через App Store Server Notifications V2, и большинство приложений обрабатывают только два. REFUND говорит отозвать доступ, REFUND_DECLINED означает сохранить продажу, а REFUND_REVERSED возвращает продажу и просит восстановить то, что вы отняли. Вот что требует каждое из них.

Главное
- Apple отправляет четыре сообщения, связанных с возвратом, через App Store Server Notifications V2. CONSUMPTION_REQUEST запрашивает ваши данные, а REFUND, REFUND_DECLINED и REFUND_REVERSED сообщают результат после того, как Apple уже приняла решение.
- Уведомление REFUND означает, что App Store вернул деньги за транзакцию. Оно несёт revocationDate и revocationReason, и это ваш сигнал отозвать право доступа, привязанное к этой одной транзакции, а не к каждой покупке этого продукта.
- revocationReason имеет два значения. 1 означает, что возврат был предоставлен из-за проблемы с вашим продуктом, а 0 означает, что он был предоставлен по другой причине. Значение о проблеме, это сигнал о качестве, который стоит фиксировать и отслеживать в динамике.
- REFUND_DECLINED означает, что Apple отклонила возврат клиенту. Вы сохраняете продажу и ничего не меняете, что безопасно только если вы не отозвали доступ до того, как решение стало окончательным.
- REFUND_REVERSED означает, что Apple отменила ранее предоставленный возврат, обычно после того, как клиент оспорил его. Поля отзыва исчезают из транзакции, и указание самой Apple таково: если вы отозвали контент, вам нужно его восстановить.
- Отвечайте на все четыре уведомления с HTTP 200. Если ваш сервер был недоступен и пропустил одно, эндпоинт Get Refund History позволяет найти возвращённые транзакции по идентификатору транзакции и сверить данные.
- Возврат за прошлый период подписки не всегда означает, что доступ должен закончиться. Если более новый оплаченный период всё ещё активен, отзыв по старой транзакции отрежет клиента, который в порядке с оплатой.
Apple принимает решение о вашем возврате, а затем продолжает говорить. Как только результат определён, App Store отправляет вашему серверу одно из трёх уведомлений о возврате, и каждое требует своего действия. REFUND говорит, что деньги ушли и вам следует убрать доступ. REFUND_DECLINED говорит, что клиент проиграл запрос и вы сохраняете продажу. REFUND_REVERSED говорит, что Apple отменила ранее предоставленный возврат, так что продажа снова ваша и вам нужно вернуть всё, что вы отняли. Большинство приложений подключают первое и тихо игнорируют остальные два. Именно так платящий клиент оказывается заблокированным от того, за что он заплатил.
Эти три отличаются от CONSUMPTION_REQUEST, единственного сообщения о возврате, которое просит вас ответить. Уведомления после решения не хотят спора. Им нужен HTTP 200 и правильное изменение доступа клиента. Вот что означает каждое из них, точные поля, которые несут факты, и где утекают деньги, когда вы обрабатываете их неправильно.
Четыре уведомления о возврате, и какое из них хочет ответа
App Store Server Notifications V2, это единый поток. Вы направляете его на один URL, и Apple отправляет туда каждый тип уведомления, так что вы уже получаете все четыре сообщения о возврате, обрабатываете вы их или нет. Четыре типа касаются возвратов, и только один из них является вопросом.
| Уведомление | Что сообщает вам Apple | Ваше действие | Ожидается ответ |
|---|---|---|---|
| CONSUMPTION_REQUEST | Клиент запросил возврат, и Apple хочет ваши данные | Отправьте Send Consumption Information в течение 12 часов | Да, реальные данные |
| REFUND | App Store вернул деньги за транзакцию | Отзовите право доступа для этой транзакции | Нет, HTTP 200 |
| REFUND_DECLINED | App Store отклонил возврат | Сохраните доступ, ничего не меняйте | Нет, HTTP 200 |
| REFUND_REVERSED | Apple отменила предоставленный ею возврат | Восстановите контент, который вы отозвали | Нет, HTTP 200 |
Что на самом деле сообщает уведомление REFUND
REFUND срабатывает, когда App Store успешно вернул деньги за транзакцию клиенту. Оно применяется к любому типу покупки: расходуемой, нерасходуемой, автообновляемой подписке и необновляемой подписке. Подписанная транзакция внутри уведомления теперь несёт два поля, которых не было до возврата, и эти два поля, это вся суть.
revocationDate и revocationReason несут факты
revocationDate, это время UNIX в миллисекундах, когда App Store вернул деньги за транзакцию или отозвал её. revocationReason сообщает вам категорию возврата и принимает ровно два значения.
| revocationReason | Значение по Apple | Что из этого читать |
|---|---|---|
| 1 | Возврат предоставлен из-за проблемы с продуктом | Сигнал о качестве или доставке. Фиксируйте его, отслеживайте в динамике и ищите закономерность в одном продукте или одной сборке |
| 0 | Возврат предоставлен по другой причине | Обычный возврат. Отзовите право доступа и двигайтесь дальше |
Наличие revocationDate у транзакции само по себе является флагом. Если вы позже запросите транзакцию и у неё есть revocationDate, эта покупка была возвращена, с уведомлением или без. Читайте причину рядом с ней, чтобы волна возвратов со значением 1 на одном релизе не проскользнула мимо вас как шум.
Отзывайте по транзакции, а не по продукту
Ловушка здесь в том, чтобы отозвать слишком много. REFUND называет одну транзакцию. Оно не говорит вам отключить каждую покупку, которую клиент когда-либо совершал по этому идентификатору продукта. Указание самой Apple, проверить, какой доступ у клиента всё ещё есть, прежде чем что-либо отрезать, потому что права доступа пересекаются. Классический случай, подписка: возврат приходит на продление прошлого месяца, пока продление этого месяца активно и полностью оплачено. Отзовите по продукту, и вы только что отрезали текущего платящего клиента из-за возврата за период, который уже закончился.
REFUND_DECLINED означает, что вы уже выиграли, так что не отменяйте это
REFUND_DECLINED приходит, когда App Store отклонил запрос клиента на возврат. Клиент попросил, Apple сказала нет, и вы сохраняете продажу. На первый взгляд делать нечего, и в этом суть. Ошибка, которую обнажает это уведомление, другая: ранний отзыв доступа.
Если ваш код реагирует на CONSUMPTION_REQUEST, убирая доступ клиента до того, как Apple вынесет решение, REFUND_DECLINED, это момент, когда это решение взрывается. Apple оставила ваши деньги, а вы заблокировали клиента, чей возврат был отклонён. Теперь этот клиент платит за продукт, которым не может пользоваться, открывает обращение в поддержку и запоминает это. Исправление, это правило, а не функция: отзывайте по REFUND, никогда по запросу. REFUND_DECLINED, это просто подтверждение Apple, что ранний отзыв был бы неверным решением.
REFUND_REVERSED, это уведомление, которое возвращает вам деньги
REFUND_REVERSED, это то, которое почти никто не обрабатывает, и именно оно возвращает деньги вам. Apple отправляет его, когда отменяет ранее предоставленный возврат, обычно после того, как клиент оспорит этот возврат. Поля отзыва, которые REFUND добавил в транзакцию, снова удаляются, так что покупка снова читается как оплаченная. Apple формулирует задачу разработчика одной строкой: если ваше приложение отозвало контент или услуги в результате связанного возврата, их нужно восстановить. Это применяется к любому типу покупки, от расходуемой до автообновляемой подписки.
Проблема недель спустя
Настоящий вопрос, который поднимают разработчики на собственных форумах Apple, это тайминг. REFUND_REVERSED может прийти через недели после исходного REFUND, спустя долгое время после того, как период подписки истёк. Восстанавливать ли доступ тогда? Восстановите то, что транзакция действительно предоставляет, ограничившись тем, что покрывает эта транзакция. Для расходуемой или нерасходуемой покупки включите разблокировку обратно. Для периода подписки, который уже прошёл, вы не выдаёте новое время, вы исправляете запись, чтобы история клиента была точной, и любое всё ещё действительное право доступа снова заработало. Восстановите конкретную транзакцию, а ваша логика пересечений решит, что активно сейчас.

Где здесь деньги, если сделать всё правильно
Каждое из этих уведомлений соответствует реальному числу, и цена неправильной обработки, это не только цена продажи.
REFUND: перестаньте платить за обслуживание клиента, которому вернули деньги
Цена продажи потеряна в момент, когда приходит REFUND. Что вы всё ещё можете контролировать, это стоимость продолжения обслуживания. Каждый час, что возвращённое право доступа остаётся активным, вы продолжаете тратить на то, за что клиент больше не платит: вычисления, вызовы API моделей, хранилище и любые выплаты авторам или партнёрам, привязанные к его использованию. Своевременный отзыв по REFUND останавливает этот счётчик. Игнорирование уведомления означает, что вы финансируете продукт для того, с кем магазин уже рассчитался.
REFUND_DECLINED: не превращайте выигрыш в возврат доброй воли
Когда вы отзываете доступ рано, а возврат позже отклоняют, вы сохранили продажу на бумаге и потеряли её на деле. Клиент, который заплатил, не может пользоваться продуктом, так что вы наследуете разговор с поддержкой и, часто, добровольный возврат, чтобы всё исправить. Это оплата дважды за одну продажу, которой ничто не угрожало. Правильная обработка REFUND_DECLINED не стоит ничего, и именно поэтому не трогать доступ до REFUND, это самое дешёвое правило, которое вы можете принять.
REFUND_REVERSED: худшее сочетание, это когда потеряны и их деньги, и их доступ
Проигнорируйте REFUND_REVERSED, и вы придёте к худшему исходу на доске. Вам заплатили, а у клиента ничего нет. Он уже один раз обращался в банк, чтобы отменить возврат, а человек, заблокированный от продукта, за который с него теперь берут деньги, это человек, который, вероятно, обратится в банк во второй раз. Этот следующий спор может стать чарджбэком по карте, который окончателен на стороне банка и стоит дороже, чем когда-либо стоила продажа. Восстановление доступа в момент прихода REFUND_REVERSED, это самая дешёвая страховка во всём процессе возврата.
Что нужно подключить
Обработка невелика, когда модель верна. Привязывайте права доступа к идентификатору транзакции, чтобы каждое уведомление указывало на одну покупку. На CONSUMPTION_REQUEST отправляйте свои данные в течение 12 часов. На REFUND отзывайте эту транзакцию. На REFUND_DECLINED не делайте ничего. На REFUND_REVERSED восстанавливайте. Быстро возвращайте HTTP 200 на все из них и делайте изменение доступа в своё время.
Для разрыва, который оставляют уведомления, используйте эндпоинт Get Refund History. Если ваш сервер был недоступен во время сбоя и пропустил REFUND, вызовите поиск возвратов App Store Server API по идентификатору транзакции по адресу /inApps/v2/refund/lookup/{transactionId} и прочитайте подписанные транзакции с их revocationDate и revocationReason. Он сверяет по одной транзакции за раз и постранично проходит по возвращённым покупкам клиента, так что пропущенный вебхук не становится навсегда неправильно установленным правом доступа.
Это та часть, которую RefundHalt выполняет за вас. Он слушает все четыре типа, отзывает по REFUND, оставляет доступ нетронутым по REFUND_DECLINED и автоматически восстанавливает по REFUND_REVERSED, каждый привязанный к точной транзакции. Отменённый возврат не лежит в очереди, пока платящий клиент остаётся заблокированным, а отклонённый никогда не запускает отзыв, который пришлось бы откатывать.
Частые вопросы
- В чём разница между REFUND и REFUND_REVERSED?
- REFUND означает, что App Store вернул деньги за транзакцию и вам следует отозвать это право доступа, тогда как REFUND_REVERSED означает, что Apple отменила предоставленный ею возврат и вам следует восстановить отозванный контент. Эти два составляют пару: покупка может пройти REFUND, а затем, если спор клиента отменяется, REFUND_REVERSED. Привязывайте изменения доступа к идентификатору транзакции, чтобы каждое уведомление действовало на правильную покупку.
- Нужно ли что-то отправлять в ответ на уведомление REFUND?
- Нет. Вы отвечаете на REFUND, REFUND_DECLINED и REFUND_REVERSED с HTTP 200 и без тела. Только CONSUMPTION_REQUEST просит вас отправить данные, и делает это через эндпоинт Send Consumption Information в течение 12 часов. Остальные три, это Apple, сообщающая о решении, а не задающая вопрос.
- Что делать при получении уведомления REFUND_DECLINED?
- Ничего не меняется, потому что возврат клиенту был отклонён и вы сохраняете продажу. Единственный способ, которым REFUND_DECLINED вызывает работу, это если вы отозвали доступ рано, до решения Apple. Отзывайте по REFUND, а не по CONSUMPTION_REQUEST, и REFUND_DECLINED станет подтверждением, что доступ был правильно оставлен нетронутым.
- Восстанавливать ли доступ, когда REFUND_REVERSED приходит через недели после возврата?
- Да, восстановите право доступа, которое предоставляет эта конкретная транзакция. Apple заявляет, что если ваше приложение отозвало контент из-за связанного возврата, его нужно восстановить. Для расходуемой или нерасходуемой покупки включите разблокировку обратно. Для периода подписки, который уже истёк, вы исправляете запись, а не выдаёте новое время, так что ваша логика пересечений всё равно решает, что активно сейчас.
- Как поймать уведомление о возврате, которое пропустил мой сервер?
- Используйте эндпоинт Get Refund History в App Store Server API, который ищет возвращённые транзакции клиента по идентификатору транзакции по адресу /inApps/v2/refund/lookup/{transactionId}. Он возвращает подписанные транзакции с revocationDate и revocationReason, так что после сбоя вы можете сверить доступ, не дожидаясь уведомления, которое уже сработало. Он обрабатывает один идентификатор транзакции за вызов и постранично проходит по возвращённым покупкам клиента.
Источники и дополнительное чтение
RefundHalt
Автопилот возвратов для App Store и Google Play
Читайте дальше
Теперь каждый запрос на возврат от Apple приходит с причиной, и consumptionRequestReason показывает, как её прочитать
Начиная с WWDC24, каждый Apple CONSUMPTION_REQUEST несёт consumptionRequestReason, собственную заявленную причину клиента, по которой он хочет возврат. Значений пять, от UNINTENDED_PURCHASE до LEGAL, и каждое должно менять то, что вы отправляете в ответ в течение вашего 12-часового окна. Вот как прочитать каждое из них.
Разбор возвратного платежа в Google Play дает вам 24 часа на защиту, вот что нужно отправить
Когда банк отзывает платеж в Google Play, Google отправляет на ваш сервер PendingRefundReviewNotification и запускает 24-часовой отсчет. Ответьте через API ReviewRefund с предпочтением по возврату и реальными доказательствами потребления, или спор решится без вас. Вот весь процесс, поле за полем.