Все статьи
Подробный разбор8 мин чтения

Проверяйте серверные уведомления App Store, прежде чем реагировать на возврат, ведь отправить запрос на ваш URL вебхука может кто угодно

Уведомление о возврате остается обычным HTTPS-запросом, пока вы не проверили его подпись. Рассказываем, как проверять серверные уведомления App Store и сообщения RTDN из Google Play, что подписывает каждый магазин и во что обходится поддельный или ошибочно отклоненный возврат.

Латунная печать для сургуча и запечатанный конверт рядом с ноутбуком на столе в теплом свете лампы, образ того, почему серверные уведомления App Store нужно проверять, прежде чем им доверять

Главное

  • Apple отправляет каждое серверное уведомление App Store версии 2 в виде signedPayload, то есть JSON Web Signature из трех частей в Base64URL: заголовка, полезной нагрузки и подписи.
  • Заголовок JWS серверного уведомления App Store содержит цепочку сертификатов x5c из трех сертификатов: конечного подписывающего сертификата, промежуточного сертификата Apple с расширением Apple Worldwide Developer Relations 1.2.840.113635.100.6.2.1 и корневого сертификата Apple.
  • App Store Server Library от Apple проверяет уведомления с помощью SignedDataVerifier, который сверяет цепочку сертификатов, идентификатор пакета, environment, а в продакшене и appAppleId, а также умеет проверять отзыв сертификатов онлайн.
  • Google Play Real-time developer notifications не подписываются Google Play. В push-подписке Pub/Sub может передавать подписанный токен OpenID Connect в заголовке Authorization, а ваш сервер проверяет его подпись, аудиторию (audience) и утверждения об адресе электронной почты.
  • По словам Google, RTDN лишь сообщает, что состояние покупки изменилось, поэтому безопасная реакция на любое уведомление о возврате состоит в том, чтобы подтвердить его через Google Play Developer API, прежде чем отзывать доступ.
  • Apple отправляет серверные уведомления App Store из подсети 17.0.0.0/8, но список разрешенных IP-адресов лишь сужает круг тех, кто может обратиться к вашей конечной точке. Доказательством того, что сообщение пришло от Apple, служит подпись.
  • Верификатор, который ошибочно отклоняет подлинные сообщения, обходится так же дорого, как тот, что принимает подделки: на отброшенный CONSUMPTION_REQUEST отводится 12 часов для ответа, а на рассмотрение чарджбэка в Google Play 24 часа.

Любой, кто узнал URL вашего вебхука, может отправить на него возврат. Это неприятный факт, стоящий за каждым серверным уведомлением App Store и каждым Google Play Real-time developer notification: пока вы его не проверили, сообщение остается обычным HTTPS POST с небольшим JSON внутри. Если ваш сервер отменяет подписку, списывает кредиты или блокирует аккаунт потому, что в запросе было написано REFUND, вы передали эту власть любому, кто умеет написать команду curl.

Оба магазина дают способ проверки. Apple подписывает само уведомление, поэтому проверить серверные уведомления App Store можно, имея лишь корневой сертификат Apple. Google Play идет другим путем. Само сообщение не подписано, но доставка через Pub/Sub может быть подписана, и Google в любом случае требует подтверждать каждое изменение через свой API. Ниже о том, что доказывает каждый способ и чего стоит ошибка в ту или другую сторону.

Что на самом деле подписывает каждый магазин

ВопросApp Store Server Notifications V2Google Play RTDN
Подписано ли само сообщение?Да, signedPayload является JWSНет, поле данных содержит JSON в кодировке base64
Что подтверждает отправителяЦепочка сертификатов x5c до корневого сертификата AppleТокен OIDC от Pub/Sub при push или ваш собственный аутентифицированный pull
Вложенные подписанные объектыsignedTransactionInfo и signedRenewalInfo, каждый в виде отдельного JWSНет
Поля для сверки с вашим приложениемbundleId, appAppleId, environmentpackageName
Исходная сеть17.0.0.0/8, песочница и продакшенGoogle Cloud Pub/Sub
Подтверждение перед действиемНеобязательно, подписанные данные и есть записьОбязательно, Google требует вызвать Developer API
ТранспортHTTPS, TLS 1.2 или новееHTTPS push или pull через клиентскую библиотеку

Как проверять серверные уведомления App Store

В документации Apple signedPayload описан как строка из трех компонентов в кодировке Base64URL, разделенных точками: заголовка, полезной нагрузки и подписи, подписанная по спецификации JSON Web Signature. Декодировать полезную нагрузку несложно. Чтобы ей доверять, нужны четыре проверки.

  • Прочитайте массив x5c из декодированного заголовка. Apple документирует порядок: сначала сертификат с ключом подписи, затем промежуточный сертификат Apple, затем корневой сертификат Apple.
  • Проверьте эту цепочку относительно корневого сертификата Apple, который вы сами скачали с PKI-сайта Apple, на практике это Apple Root CA - G3. Никогда не доверяйте корневому сертификату, пришедшему внутри сообщения, ведь злоумышленник может поместить туда любой сертификат.
  • Убедитесь, что промежуточный сертификат содержит расширение Apple Worldwide Developer Relations 1.2.840.113635.100.6.2.1, затем проверьте подпись JWS открытым ключом конечного сертификата.
  • Сверьте декодированные данные со своим приложением: bundleId, appAppleId в продакшене и environment. Действительная подпись Apple на приложении другого разработчика или на покупке из песочницы, попавшей на вашу боевую конечную точку, все равно не означает ваш возврат.

Затем повторите то же самое для вложенных данных. Объект data содержит signedTransactionInfo и signedRenewalInfo, и каждый из них является отдельным JWS со своей цепочкой x5c. Обработчик REFUND, который проверяет конверт, но читает транзакцию без проверки, оставляет брешь посередине.

Используйте библиотеку Apple, а не пишите все сами

Apple публикует App Store Server Library для Swift, Java, Python и Node. Ее SignedDataVerifier принимает ваши корневые сертификаты, флаг онлайн-проверок, environment, идентификатор пакета, а для продакшена и appAppleId. Исходный код для Node показывает, что он требует: ровно три сертификата в цепочке, расширение конечного сертификата Apple 1.2.840.113635.100.6.11.1 и расширение промежуточного сертификата, действующие сроки сертификатов, а также совпадение идентификатора пакета, идентификатора приложения и environment. При включенных онлайн-проверках он также спрашивает OCSP-ответчик, не были ли отозваны конечный и промежуточный сертификаты.

Ювелирная лупа над бумажным сертификатом с тисненой печатью на деревянном столе, именно так внимательно стоит изучить сообщение о возврате, прежде чем ему доверять

Достаточно ли списка разрешенных IP-адресов?

Нет. Apple указывает, что если вашему серверу нужен список разрешенных адресов, в него следует добавить подсеть 17.0.0.0/8, причем это касается и песочницы, и продакшена. Это снижает шум. Но это не доказывает подлинность сообщения и перестает работать, как только сеть Apple меняется так, что вы об этом не узнали. Настоящим барьером должна оставаться проверка подписи.

Как проверять уведомления о возвратах из Google Play

Google Play публикует сообщения RTDN в принадлежащую вам тему Pub/Sub, и роль Pub/Sub Publisher в ней должна быть только у сервисного аккаунта google-play-developer-notifications@system.gserviceaccount.com. Затем ваша подписка доставляет сообщения одним из двух способов, и у каждого свое доказательство.

  • Pull-подписка: ваш сервер открывает аутентифицированное соединение с Pub/Sub и забирает сообщения. Публичной конечной точки нет, поэтому постороннему некуда отправлять запросы.
  • Push-подписка с включенной аутентификацией: Pub/Sub подписывает JSON Web Token и отправляет его в заголовке Authorization как токен Bearer. Руководство Google предписывает проверять подпись токена и убеждаться, что утверждения email и audience совпадают с настроенными вами значениями, а пример кода дополнительно требует, чтобы email_verified было равно true. Токену может быть до одного часа.
  • Push-подписка без аутентификации: все, что может обратиться к URL, может отправить сообщение, похожее на RTDN.

Почему Google требует подтверждать через API

Даже идеально аутентифицированное RTDN не рассказывает всей истории. В руководстве Google по настройке прямо сказано, что эти уведомления сообщают лишь о том, что состояние покупки изменилось, а для получения полного статуса нужно вызвать Google Play Developer API. Для voidedPurchaseNotification это означает найти покупку в Voided Purchases API или получить ее текущее состояние, прежде чем что-либо отзывать. Вызов API аутентифицируется вашим собственным сервисным аккаунтом, поэтому подделать его ответ злоумышленник не сможет.

Во что обходится поддельный или отклоненный возврат

Поддельное уведомление о возврате, которому вы поверили, стоит вам платящего клиента. Вы блокируете человека, чьи деньги все еще у вас, он открывает обращение в поддержку, а некоторые идут в магазин и просят возврат, который ваша система уже якобы оформила. Поддельная покупка или продление обходятся в обратную сторону: вы предоставляете вычисления на GPU, платные вызовы сторонних API, хранилище и выплаты авторам под выручку, которой никогда не было.

Обратный сбой тише и часто крупнее. Верификатор, который отклоняет подлинные сообщения из-за неверного appAppleId, настройки песочницы в продакшене или устаревшего корневого сертификата, теряет именно те два процесса возврата, в которых важны ваши доказательства. Apple запрашивает данные о потреблении в течение 12 часов после CONSUMPTION_REQUEST. Google дает 24 часа на ответ на рассмотрение чарджбэка через orders.reviewrefund, а для заказов, оформленных после 3 августа 2026 года, проигранный чарджбэк в Google Play обойдется вам в цену покупки за вычетом сервисного сбора Play плюс комиссию, которую взимает банк покупателя.

СбойЧто происходитВо что обходится
Принят поддельный REFUNDПлатящий клиент теряет доступВремя поддержки и настоящий запрос на возврат, который вы сами спровоцировали
Принята поддельная покупкаБесплатный доступ к платным функциямВычисления, вызовы API, хранилище и выплаты без выручки
Отклонен подлинный CONSUMPTION_REQUESTApple решает без ваших данныхПродажа плюс все, что уже потрачено на ее исполнение
Отклонено подлинное рассмотрение чарджбэкаGoogle решает без ваших доказательствЦена покупки и банковские комиссии по заказам после 3 августа 2026 года

Не позволяйте ошибке проверки съесть срок

  • Сохраняйте исходный запрос до проверки, чтобы отклоненное сообщение никогда не терялось окончательно.
  • Настройте оповещение на каждую неудачную проверку. Одно поддельное сообщение это шум. Если разом не проходят все сообщения, ошибка на вашей стороне.
  • В Apple отклоненное можно обработать повторно. Get Notification History возвращает исходный signedPayload за последние 180 дней, так что исправьте верификатор и снова прогоните через него сохраненные или восстановленные полезные нагрузки.
  • В Google ожидающее рассмотрение чарджбэка существует только в своем уведомлении, поэтому неверно настроенная audience для push означает пропущенное рассмотрение. Проверяйте это через Send Test Message в Play Console после каждого изменения.

RefundHalt проверяет уведомления обоих магазинов, подтверждает возвраты через API магазинов, прежде чем что-либо изменится, и отвечает на CONSUMPTION_REQUEST и рассмотрения чарджбэков в отведенные сроки. Поэтому поддельное сообщение не будет стоить вам клиента, а настоящее не проскользнет незамеченным.

Частые вопросы

Как проверить серверное уведомление App Store?
Проверьте signedPayload как JSON Web Signature. Прочитайте цепочку сертификатов x5c из его заголовка, проверьте ее относительно корневого сертификата Apple, скачанного с PKI-сайта Apple, проверьте подпись ключом конечного сертификата, а затем убедитесь, что bundleId, appAppleId и environment соответствуют вашему приложению. App Store Server Library от Apple делает все это в SignedDataVerifier.
Какой ключ использовать для проверки серверных уведомлений App Store?
Ключ из App Store Connect вам не нужен. Открытый ключ находится в первом сертификате цепочки x5c в заголовке JWS, и доверять ему можно только после проверки цепочки вплоть до корневого сертификата Apple, полученного вами самостоятельно. Ваш ключ для встроенных покупок нужен для вызова API Apple, а не для проверки уведомлений.
Подписаны ли Google Play Real-time developer notifications?
Нет, Google Play не подписывает само сообщение RTDN. В аутентифицированной push-подписке Pub/Sub отправляет в заголовке Authorization подписанный токен OpenID Connect, который вы проверяете, а Google рекомендует после каждого уведомления вызывать Google Play Developer API, чтобы получить полный статус покупки.
Должен ли мой сервер возвращать ошибку, если уведомление не прошло проверку?
Сначала сохраните исходный запрос, затем решайте. Apple повторяет уведомление версии 2 после ответа 40x или 50x через 1, 12, 24, 48 и 72 часа после предыдущей попытки. Если причина в вашей собственной конфигурации, Get Notification History позволяет в течение 180 дней получить исходный signedPayload и проверить его снова после исправления.
Нужно ли проверять уведомления, если я разрешаю только диапазон IP-адресов Apple?
Да. Apple просит серверы со списком разрешенных адресов принимать подсеть 17.0.0.0/8. Это ограничивает круг тех, кто может подключиться, но не доказывает подлинность сообщения. Именно подпись JWS связывает уведомление с Apple и с вашим приложением.

Источники и дополнительное чтение

RefundHalt

Автопилот возвратов для App Store и Google Play

Читайте дальше

Руководство8 мин чтения

Чрезмерные запросы на возврат обходятся дороже самих возвратов: оба магазина приложений считают их сигналом качества

Правило 5.6.4 Apple прямо называет чрезмерные запросы на возврат признаком того, что приложение не соответствует стандартам качества, а соглашение Google о распространении позволяет удалять приложения новых разработчиков по доле возвратов. Разбираем, что пишет каждый магазин, во что обходится удаление и как не попасть под удар.

Руководство9 мин чтения

Пропущенные уведомления о возвратах могут стоить вам тех единственных возвратов, которые можно оспорить. Оба магазина позволяют их вернуть, если вы проверите вовремя

Если ваш сервер пропустил уведомление о возврате, два процесса, в которых магазин запрашивает ваши доказательства, закрываются без вас. Разбираем, как Apple и Google Play повторяют доставку, за какой срок можно восстановить пропущенные уведомления о возвратах и где остаются пробелы.

Следующий запрос на возврат уже в пути.

Настройте RefundHalt за то время, что уходит на чтение очередного письма в поддержку о возврате, который вы не успели оспорить.