Почему подписка “активна” у вас, но “сброшена” в App Store и Google Play
Источник истины для подписки должен быть один, а магазины — только каналы сигналов. Иначе вы получите расхождение статусов: клиент платит, а ваш backend уже поставил pause; или наоборот — renewal прошел, но entitlement не выдан из-за потерянного webhook. Идемпотентность в биллинге — это не рекомендация, а базовый вопрос выживания системы.
Синхронизацию строят вокруг event-driven модели:
— входящие события магазина пишутся в append-only журнал;
— каждое событие проходит дедупликацию по purchase_token / original_transaction_id и версии состояния;
— переходы статусов разрешены только через конечный автомат, без “ручного” перепрыгивания из expired в active.
Тогда повторные уведомления, out-of-order delivery и ретраи не ломают консистентность.
Для Apple и Google критичны разные edge cases. У Apple часто важнее корректно обрабатывать delayed notifications и family sharing, у Google — revoke, grace period и pause. Нельзя хранить “последний статус” как строку без контекста: нужен атомарный набор полей — current_state, source_state, last_event_at, entitlement_until, reconciliation_flag. Иначе reconciliation превращается в гадание по логам.
Нужен отдельный reconciliation job: он сверяет локальное состояние с подтвержденными фактами магазина и поднимает в очередь только расхождения. Не пытайтесь чинить расхождения синхронным запросом в момент чекаута: это увеличивает latency, а в сетевом сплите шлюза легко получить двойную активацию или потерю продления.
Сильная синхронизация подписок — это не про “получили webhook и обновили запись”, а про устойчивую модель состояния, где любой дубль, задержка или пропуск безопасно переживаются системой.
Подписки: биллинг-лаб
@subscriptions_billing_lab_arb
Почему подписка “активна” у вас, но “сброшена” в App Store и Google Play
Этот пост опубликован в Telegram-канале Подписки: биллинг-лаб. Подписаться можно по ссылке: @subscriptions_billing_lab_arb.