Все статьи
Deep dive7 мин чтения

Есть один эндпоинт, который возвращает всю историю возвратов клиента в App Store, и вот что он выдаёт

Эндпоинт Apple Get Refund History возвращает полную историю возвратов клиента в App Store в виде подписанных транзакций. Вот каждое поле, как токен revision разбивает выдачу на страницы, почему она привязана к клиенту, а не к приложению, и во что вам обходится пропущенный возврат.

Вид сверху на рабочий стол со смартфоном, бумажным журналом транзакций и лупой, иллюстрирующий получение истории возвратов клиента в App Store через эндпоинт Apple Get Refund History

Главное

  • Get Refund History это эндпоинт App Store Server API, который возвращает возвращённые встроенные покупки клиента в вашем приложении в виде списка подписанных транзакций, чтобы вы могли сверять возвраты и отзывать доступ, даже когда уведомление до вас так и не дошло.
  • Вы вызываете GET /inApps/v2/refund/lookup/{transactionId} с любым идентификатором транзакции этого клиента, и Apple возвращает его возвраты по всем типам покупок в вашем приложении, а не только по той, о которой вы спрашивали.
  • В ответе три поля: signedTransactions, до 20 JWS-транзакций на страницу, отсортированных так, что самый ранний возврат идёт первым, плюс токен revision и булев признак hasMore для постраничной выдачи.
  • Сохраните последний токен revision. Передайте его в следующий раз, и Apple вернёт только возвраты новее этой точки, что превращает полную выгрузку истории в короткий список новых строк при каждом запуске.
  • Каждая декодированная транзакция несёт revocationDate и revocationReason. Значение revocationReason, равное 1, означает, что клиент оформил возврат из-за фактической или предполагаемой проблемы в вашем приложении, а 0 означает другую причину, например случайную покупку.
  • Эндпоинт работает по клиенту, а не по приложению. Нет ни одного вызова, который перечислял бы все возвраты по всему вашему приложению, поэтому вы сверяете данные по каждому аккаунту, отталкиваясь от идентификатора транзакции, либо читаете свою ленту уведомлений REFUND, чтобы получить картину по всему приложению.
  • Причина всё это настроить это деньги. Пропущенный возврат оставляет аккаунт активным, и вы продолжаете платить за вычисления, вызовы model API, хранилище и выплаты за клиента, с которым App Store уже полностью рассчитался.

Apple ведёт доступный для запросов учёт каждого возврата, который она одобрила по аккаунту клиента для вашего приложения, и один вызов возвращает его. Этот эндпоинт называется Get Refund History, он входит в App Store Server API и выдаёт вам полную историю возвратов этого клиента в App Store в виде списка подписанных транзакций. Вы передаёте идентификатор транзакции, получаете обратно то, что Apple вернула, и сверяете это с тем, что у вас всё ещё включено.

Вот зачем это нужно. Возврат, который вы не видите, это возврат, за который вы продолжаете платить. Деньги уже ушли, но аккаунт остаётся активным, и каждый час, пока это так, вы продолжаете тратить на вычисления, вызовы model API, хранилище и любые выплаты, привязанные к этому клиенту. Ваши уведомления о возвратах должны ловить это в момент, когда оно происходит. Get Refund History это подстраховка на случай, когда они не сработали: после сбоя, деплоя, который потерял вебхук, или обращения в поддержку, где вам нужна вся картина в одном вызове.

Что возвращает эндпоинт истории возвратов в App Store

Вы вызываете GET /inApps/v2/refund/lookup/{transactionId} к App Store Server API, подписав его тем же JWT, что и любой другой вызов к нему. Идентификатор транзакции в пути может быть любой транзакцией этого клиента. Apple читает его как идентичность, а не как фильтр, и возвращает возвращённые покупки этого клиента по всему вашему приложению: расходуемые, нерасходуемые, авто-возобновляемые и невозобновляемые подписки в равной мере. Более старая версия V1 этого эндпоинта возвращала до 50 возвратов в одном ответе и объявлена устаревшей. Текущая версия разбивает выдачу на страницы, поэтому вы обслуживаете клиентов с длинной историей без гигантского ответа.

Ответ состоит из трёх полей

FieldЧто содержит
signedTransactionsДо 20 возвращённых транзакций этого клиента, каждая как подписанный JWS, который вы проверяете и декодируете. Отсортированы так, что самый ранний возврат идёт первым, по revocationDate. Пустой массив означает, что у клиента нет возвратов в вашем приложении
revisionТокен постраничной выдачи. Передайте его обратно, чтобы получить следующую страницу, и сохраните последний, чтобы в следующий раз получить только новые возвраты
hasMoreИстина, когда у Apple больше возвращённых транзакций, чем вернула эта страница, поэтому вы вызываете снова с revision

Что говорит вам одна возвращённая транзакция

Каждая запись в signedTransactions это JWS. Проверьте её по цепочке сертификатов Apple, декодируйте, и вы получите обычную полезную нагрузку транзакции с заполненными полями возврата. Здесь важны именно эти поля.

FieldЧто говорит вам
transactionIdИдентификатор возвращённой транзакции, ваш ключ связи назад к покупке, которую вы записали
originalTransactionIdИдентификатор первой покупки в цепочке, то, как вы связываете продления подписки вместе
productIdПродукт, по которому оформлен возврат, чтобы вы отозвали именно то право доступа и ничего лишнего
revocationDateВремя в формате UNIX, в миллисекундах, когда Apple вернула транзакцию
revocationReasonПочему Apple оформила возврат. 1 означает фактическую или предполагаемую проблему с вашим приложением, 0 означает другую причину, например случайную покупку
price, currencyСумма в milliunits и её код валюты по ISO 4217, чтобы вы могли подсчитать возвращённые деньги
appAccountTokenUUID, который вы прикрепили при покупке, самый чистый способ сопоставить возврат с вашим собственным пользователем

Токен revision это то, как вы перестаёте перечитывать весь список

Наивный способ использовать этот эндпоинт это искать клиента и обходить каждую страницу каждый раз. Это работает, и на клиенте с пятьюдесятью возвратами это пятьдесят строк, которые вы уже знали, плюс одна новая. Токен revision существует, чтобы убрать эту трату. Каждый ответ несёт revision. Когда hasMore истина, вы передаёте его обратно, чтобы получить следующую страницу. Когда вы доходите до конца, вы сохраняете последний revision, который видели.

Чего этот эндпоинт делать не будет

Есть одно ожидание, от которого стоит отказаться, прежде чем строить на нём. Get Refund History работает по клиенту, а не по приложению. Вы не можете запросить у него все возвраты, которые ваше приложение получило на прошлой неделе. Он отвечает на один вопрос, какие возвраты есть у этого аккаунта, и вам нужно прийти с идентификатором транзакции этого аккаунта, чтобы задать его. Разработчики постоянно упираются в эту стену и идут искать эндпоинт возвратов по всему приложению, которого не существует.

Картина по всему приложению живёт в другом месте. Ваша лента App Store Server Notifications отправляет уведомление REFUND в момент, когда Apple одобряет каждый из них, а Get Notification History позволяет воспроизвести эту ленту с фильтром по типам возвратов за диапазон дат. Так что разделение чёткое. Уведомления и их история дают вам поток по всему приложению. Get Refund History даёт вам авторитетный список одного аккаунта по запросу, а это именно то, что вам нужно на стойке поддержки или после сбоя.

Лупа, зависшая над одной выделенной строкой бумажного журнала транзакций рядом со смартфоном, иллюстрирующая поиск возвратов одного клиента в истории возвратов App Store

Во что вам обходится пропущенный возврат в деньгах

Эндпоинт это трубопровод. Счёт это причина, по которой вы прокладываете трубу. Каждый возврат в этом списке это деньги, уже возвращённые, и единственная переменная, оставшаяся в вашем контроле, это как долго вы продолжаете тратить на аккаунт, который больше не платит.

Вы продолжаете платить за обслуживание возвращённого аккаунта

Цена покупки уходит в тот же миг, когда Apple одобряет возврат. Что продолжает работать, так это стоимость доставки. Для приложения, которое делает реальную работу на пользователя, это вычисления, вызовы model API, хранилище и любая выплата автору или партнёру, привязанная к его использованию. Возвращённый клиент, чей доступ вы так и не отключили, это подписка, которую вы финансируете из своего кармана. Сверка по Get Refund History и отзыв по тому, что вы находите, это то, как вы выключаете этот счётчик, когда уведомление проскользнуло мимо.

Причина возврата 1 это замаскированный отчёт о дефекте

revocationReason обходится вам вдвое, если вы его игнорируете. Первая цена это сам возврат. Вторая это каждый будущий возврат по той же причине. Когда продукт снова и снова возвращается с revocationReason 1, фактической или предполагаемой проблемой в вашем приложении, Apple вручает вам размеченный образец того, из-за чего клиенты просят деньги назад. Отслеживайте это по продуктам, и вы сможете залатать течь вместо того, чтобы оплачивать её по одному возврату за раз.

Поймать поздно всё равно лучше, чем не поймать

Чарджбэк окончателен с банком и, на другом сторе, теперь несёт комиссию, которую разработчик оплачивает сам. Возврат в App Store это не то же самое. Он завершён, но право доступа ваше, чтобы отозвать его в момент, когда вы узнаёте. Так что даже возврат, который вы находите с опозданием на дни через этот эндпоинт, стоит найти. Вы не можете вернуть деньги, но можете остановить трату, которая всё ещё шла позади него.

Как это стыкуется с уведомлениями и с Google

Думайте об этих частях как об одной системе. Уведомление REFUND это живой сигнал, отправляемый на ваш сервер по мере того, как Apple принимает решение. Get Refund History это источник истины по запросу для одного клиента, вызов, который вы делаете, когда push не сработал или когда человеку нужен полный аккаунт перед глазами. На стороне Google Play форма та же по идее, но с другими названиями: VoidedPurchaseNotification приходит в реальном времени, а Voided Purchases API это список, который вы вытягиваете. Оба стора дают вам поток и журнал. Ошибка в том, чтобы доверять только потоку, потому что потоки теряют данные.

Как настроить это способом RefundHalt

Цикл маленький, когда каждая часть на месте. Возьмите уведомление REFUND как триггер. Сверьте по Get Refund History, чтобы потерянный вебхук никогда не оставлял возвращённый аккаунт активным. Декодируйте каждую транзакцию, свяжите её по appAccountToken или transactionId назад к вашему пользователю, прочитайте revocationReason, чтобы дефектный возврат был помечен, а не просто подшит, и отзовите именно то право доступа, а не весь аккаунт. Листайте страницы с токеном revision, чтобы читать новые возвраты, а не старые.

Это та часть, которую RefundHalt берёт на себя. Он слушает уведомления о возвратах, откатывается на Get Refund History, когда ему нужен авторитетный список, проверяет каждую подписанную транзакцию, отзывает именно ту покупку и хранит revision, чтобы каждый проход читал только то, что изменилось. Вы получаете отключение доступа за секунды и чистую запись о том, кому оформлен возврат, за что и почему, без того чтобы поднимать опрос и проверку JWS самостоятельно.

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

Как мне увидеть все возвраты по всему моему приложению, а не только по одному клиенту?
С Get Refund History это невозможно, потому что он работает по клиенту и требует идентификатора транзакции того аккаунта, о котором вы спрашиваете. Для картины по всему приложению используйте свою ленту App Store Server Notifications, которая отправляет уведомление REFUND по каждому возврату по мере того, как Apple его одобряет, и Get Notification History, чтобы воспроизвести эту ленту с фильтром по типам возвратов за диапазон дат.
Сколько возвратов возвращает эндпоинт Get Refund History?
Текущая версия возвращает до 20 возвращённых транзакций на страницу, отсортированных так, что самый ранний возврат идёт первым, и перелистывает остальное токеном revision, когда hasMore истина. Устаревший эндпоинт V1 возвращал до 50 в одном ответе. Ограничения на общее число нет, поэтому клиент с длинной историей просто занимает больше страниц.
Для чего нужен токен revision?
Это то, как вы разбиваете выдачу на страницы и как вы избегаете перечитывания всей истории клиента каждый раз. Каждый ответ включает revision. Вы передаёте его обратно, чтобы получить следующую страницу, и сохраняете последний, чтобы ваш следующий поиск вернул только возвраты новее этой точки. Это удерживает плановую сверку в рамках короткого списка новых строк.
Что означает revocationReason в возвращённой транзакции?
Это причина, по которой Apple оформила возврат транзакции. Значение 1 означает, что клиент оформил возврат из-за фактической или предполагаемой проблемы внутри вашего приложения, а 0 означает другую причину, например случайную покупку. revocationDate говорит, когда произошёл возврат, в UNIX-миллисекундах. Чтение revocationReason позволяет отделить дефект продукта от разового возврата по сожалению.
Нужно ли мне это, если я уже обрабатываю уведомления REFUND?
Да, в качестве подстраховки. Уведомления это живой сигнал, но push может не дойти во время сбоя, неудачного деплоя или изменения вебхука, и пропущенный возврат оставляет возвращённый аккаунт активным и стоящим вам денег. Get Refund History это источник истины по запросу, по которому вы сверяетесь, чтобы ничего не оставалось включённым, за что Apple уже вернула деньги.

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

RefundHalt

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

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

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

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