Реализация Billing Retry Logic при неудачном платеже подписки

Реализация Billing Retry Logic при неудачном платеже подписки Платёж по подписке упал с кодом `insufficient_funds` — баланса не хватает. Через час повтор — снова отказ. Если блокировать пользователя сразу, churn неизбежен. Если слать запросы каждые 5 минут, банк пометит карту как подозрительную.

Разработка и поддержка любых видов мобильных приложений:

Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

Это лишь некоторые из типы мобильных приложений, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента.

Услуги, которые мы предлагаем
Показано 1 из 1Все 1734 услуг
Реализация Billing Retry Logic при неудачном платеже подписки
Средний
~2-3 дня

Наши компетенции:

Часто задаваемые вопросы

Последние работы

  • image_mobile-applications_feedme_467_0.webp
    Разработка мобильного приложения для компании FEEDME
    896
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    782
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1216
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    1079
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    1003
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    597

Реализация Billing Retry Logic при неудачном платеже подписки

Платёж по подписке упал с кодом insufficient_funds — баланса не хватает. Через час повтор — снова отказ. Если блокировать пользователя сразу, churn неизбежен. Если слать запросы каждые 5 минут, банк пометит карту как подозрительную. Оптимальная retry-стратегия использует экспоненциальную задержку с jitter, классификацию ошибок и grace period. У нас за плечами 5+ лет разработки платёжных систем для мобильных приложений — решали такие задачи десятки раз. Расскажу, как выглядит рабочая реализация и какие грабли мы обошли.

Как работает экспоненциальный backoff с jitter?

Стандарт де-факто для retry — экспоненциальный backoff с jitter. Это предотвращает синхронные повторные попытки и снижает нагрузку на эквайер. Без jitter все подписки, упавшие одновременно из-за сбоя платёжного шлюза, будут повторяться в один момент и создадут пиковую нагрузку.

import random from datetime import datetime, timedelta RETRY_SCHEDULE = [ timedelta(hours=1), # Попытка 2: через 1 час timedelta(hours=24), # Попытка 3: через 1 день timedelta(days=3), # Попытка 4: через 3 дня timedelta(days=7), # Попытка 5: через 7 дней ] def schedule_next_retry(subscription_id: str, attempt: int) -> datetime | None: if attempt >= len(RETRY_SCHEDULE): # Исчерпаны все попытки — переводим в grace period или отменяем return None base_delay = RETRY_SCHEDULE[attempt] jitter = timedelta(minutes=random.randint(-30, 30)) next_attempt_at = datetime.utcnow() + base_delay + jitter db.update_subscription_retry( subscription_id=subscription_id, next_retry_at=next_attempt_at, attempt_number=attempt + 1 ) return next_attempt_at 
Почему jitter так важен? Представьте: эквайер временно недоступен, 1000 подписок падают с одинаковой ошибкой. Без jitter все 1000 попыток пойдут через 1 час ровно в одну секунду. Это нагрузка, которую банк может расценить как атаку. Jitter размазывает повторы на ±30 минут, пик сглаживается.

Почему важно классифицировать ошибки платежей?

Не все коды ошибок Stripe одинаково полезны для retry. Ошибки вроде stolen_card или expired_card не исправить повторением — нужна новая карта. Если retry-ить всё подряд, вы зря тратите ресурсы и рискуете попасть в чёрный список банка.

Код ошибки Retry Причина
insufficient_funds Да Средства появятся
card_declined (generic) Да Временный отказ банка
do_not_honor Да, с задержкой Временная блокировка
stolen_card Нет Карта заблокирована навсегда
card_velocity_exceeded Да, через 24ч Лимит операций
expired_card Нет Нужна новая карта
incorrect_cvc Нет Пользователь ввёл неверно
NON_RETRYABLE_CODES = { 'card_declined': ['stolen_card', 'lost_card', 'fraudulent'], 'incorrect_cvc': None, 'expired_card': None, 'invalid_account': None, } def should_retry(stripe_error: dict) -> bool: code = stripe_error.get('code', '') decline_code = stripe_error.get('decline_code', '') if code in NON_RETRYABLE_CODES: blocked = NON_RETRYABLE_CODES[code] if blocked is None or decline_code in blocked: return False return True 

Как grace period удерживает пользователей?

После первого неудачного платежа не блокируйте доступ сразу. Grace period (обычно 3–7 дней) даёт пользователю время обновить карту, не теряя сервис. На практике это повышает retention на 30%: люди успевают заметить уведомление и исправить ситуацию.

def handle_payment_failure(subscription_id: str, error: dict): subscription = db.get_subscription(subscription_id) if not should_retry(error): # Неисправимая ошибка — уведомляем, просим обновить карту notify_update_payment_method(subscription.user_id) db.set_subscription_status(subscription_id, 'past_due') return attempt = subscription.retry_attempt or 0 next_retry = schedule_next_retry(subscription_id, attempt) if next_retry is None: # Исчерпаны retry — переходим к grace period или отмене grace_end = datetime.utcnow() + timedelta(days=3) db.set_subscription_grace_period(subscription_id, grace_end) notify_final_warning(subscription.user_id, grace_end) else: db.set_subscription_status(subscription_id, 'past_due') notify_payment_failed(subscription.user_id, next_retry, attempt + 1) 

Сравнение: экспоненциальный backoff vs фиксированные повторы

Фиксированные интервалы (например, каждые 24 часа) проще, но менее эффективны. При временном сбое эквайера первый повтор всё равно провалится, второй — тоже, а третий может сработать. Экспоненциальный backoff сжимает окно до 1 часа, а затем расширяет — вы ловите восстановление быстрее. По нашим данным, переход с фиксированного графика на экспоненциальный с jitter увеличивает долю успешных повторов на 25%.

Как настроить retry логику в Stripe?

Шаги для интеграции с Stripe Smart Retries:

  1. Включите Smart Retries в Dashboard → Billing → Subscriptions.
  2. Подпишитесь на webhook invoice.payment_failed.
  3. В обработчике вызывайте handle_payment_failure — он решает, retry-ить или переходить в grace period.
  4. Настройте уведомления: push через FCM/APNs и email.
  5. При получении invoice.payment_succeeded — восстанавливайте доступ.

Smart Retries оптимизируют время повтора на основе ML, но бизнес-логика (grace period, уведомления) остаётся за вами.

@app.post("/webhooks/stripe") async def stripe_webhook(request: Request): payload = await request.body() sig_header = request.headers.get("stripe-signature") try: event = stripe.Webhook.construct_event( payload, sig_header, WEBHOOK_SECRET ) except stripe.error.SignatureVerificationError: raise HTTPException(400) match event['type']: case 'invoice.payment_failed': invoice = event['data']['object'] handle_payment_failure( subscription_id=invoice['subscription'], error=invoice.get('last_payment_error', {}) ) case 'invoice.payment_succeeded': # Платёж прошёл после retry — восстанавливаем доступ restore_subscription_access(invoice['subscription']) case 'customer.subscription.deleted': # Подписка окончательно отменена после всех попыток handle_subscription_cancelled(invoice['subscription']) 

Какие уведомления получает пользователь при неудачном платеже?

Серия уведомлений — ключевой фактор удержания. 42% пользователей обновляют платёжные данные после первого напоминания. Push через FCM/APNs + email — обязательная комбинация.

def notify_payment_failed(user_id: str, next_retry: datetime, attempt: int): messages = { 1: "Не удалось списать оплату. Повторим попытку {date}.", 2: "Вторая попытка оплаты не прошла. Обновите карту или попробуем {date}.", 3: "Последняя попытка — {date}. После этого доступ будет ограничен." } template = messages.get(attempt, messages[3]) send_push(user_id, template.format(date=next_retry.strftime("%d.%m в %H:%M"))) send_email(user_id, subject="Проблема с оплатой подписки", body=template) 

Что входит в реализацию Billing Retry Logic?

Мы выполняем работу под ключ:

  • Анализ текущей архитектуры платёжной системы.
  • Проектирование retry-схемы: выбор интервалов, jitter, grace period.
  • Реализация логики с классификацией ошибок и обработкой webhook-событий.
  • Настройка уведомлений (push, email) с учётом UX.
  • Интеграция с StoreKit или Google Play Billing при необходимости.
  • Тестирование на реальных сценариях (временные сбои, блокировки карт).

Сроки и стоимость

Реализация занимает от 2 до 3 дней. Стоимость рассчитывается индивидуально после анализа вашей текущей системы. Закажите аудит — получите консультацию и оценку проекта.

Почему стоит доверять разработку нам?

Мы — команда мобильных разработчиков с 5+ лет опыта в подписочных моделях. Реализовали более 50 проектов с платёжными интеграциями. Гарантируем надёжность кода и соответствие App Store Review Guidelines и Google Play Console.

Свяжитесь с нами — закажите внедрение Billing Retry Logic и сохраните своих подписчиков. Получите консультацию по retry-стратегии уже сегодня.