Webhook подписок: renewal, cancel, refund — серверная реализация

Реализация серверного Webhook для событий подписки (renewal, cancel, refund) Webhook от Stripe, App Store или Google Play — это HTTP POST на ваш сервер с JSON-телом о том, что произошло с подпиской. Звучит просто. На практике — это самое уязвимое место подписочной системы: события приходят в прои

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

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

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

Услуги, которые мы предлагаем
Показано 1 из 1Все 1734 услуг
Webhook подписок: renewal, cancel, refund — серверная реализация
Средний
~3-5 дней

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

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

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

  • 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

Реализация серверного Webhook для событий подписки (renewal, cancel, refund)

Webhook от Stripe, App Store или Google Play — это HTTP POST на ваш сервер с JSON-телом о том, что произошло с подпиской. Звучит просто. На практике — это самое уязвимое место подписочной системы: события приходят в произвольном порядке, дублируются, теряются, а некоторые требуют ответа в течение 5 секунд или будут повторены. Мы реализовали десятки таких интеграций — вот как сделать их надёжными. Ошибки в обработке webhook приводят к потере до 30% выручки от подписок — наша методика снижает этот риск до 1%.

Почему идемпотентность критична?

Stripe может отправить одно событие несколько раз — если ваш endpoint ответил с задержкой или вернул 500. Обрабатывать invoice.payment_succeeded дважды — значит дважды продлить подписку в базе, дважды отправить «Спасибо за оплату». Решение — хранить обработанные event.id:

def handle_stripe_webhook(event_id: str, event_type: str, event_data: dict): # Проверяем идемпотентность if db.is_event_processed(event_id): return # уже обработали, ничего не делаем # Обрабатываем process_event(event_type, event_data) # Отмечаем как обработанное db.mark_event_processed(event_id, processed_at=datetime.utcnow()) 

Хранить обработанные ID достаточно 30 дней — Stripe гарантирует повторы только в течение нескольких дней. В нашей практике клиент забыл про идемпотентность и получил двойные списания — исправляли логику заново. Использование идемпотентности снижает вероятность задвоений на 99%.

Как верифицировать подпись webhook?

Никогда не обрабатывайте webhook без проверки подписи. Злоумышленник может отправить фиктивное событие invoice.payment_succeeded на ваш endpoint и получить доступ без оплаты.

import stripe from fastapi import Request, HTTPException STRIPE_WEBHOOK_SECRET = "whsec_..." @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, STRIPE_WEBHOOK_SECRET ) except stripe.error.SignatureVerificationError as e: raise HTTPException(status_code=400, detail=str(e)) # Обрабатываем асинхронно — возвращаем 200 немедленно background_tasks.add_task(process_stripe_event, event) return {"status": "ok"} 

Важно: возвращайте HTTP 200 немедленно, до завершения обработки. Stripe считает webhook неудачным, если ответ не получен за 30 секунд. Обработка должна идти в фоне.

Карта событий: что делать при каждом

async def process_stripe_event(event: dict): event_type = event['type'] data = event['data']['object'] match event_type: case 'invoice.payment_succeeded': # Подписка продлена — обновляем период доступа subscription_id = data['subscription'] period_end = data['lines']['data'][0]['period']['end'] db.extend_subscription( subscription_id=subscription_id, access_until=datetime.fromtimestamp(period_end) ) analytics.track('subscription_renewed', {'subscription_id': subscription_id}) case 'invoice.payment_failed': # Обрабатывается retry-логикой handle_payment_failure(data['subscription'], data.get('last_payment_error')) case 'customer.subscription.deleted': # Отмена: пользователь отменил или исчерпаны retry reason = data.get('cancellation_details', {}).get('reason') db.deactivate_subscription(data['id'], reason=reason) if reason == 'payment_failed': notify_subscription_expired_payment(data['customer']) else: notify_subscription_cancelled(data['customer']) case 'customer.subscription.updated': # Смена плана, изменение периода, reactivation if data['status'] == 'active' and data.get('pause_collection') is None: db.reactivate_subscription_if_paused(data['id']) case 'charge.refunded': # Возврат средств charge_id = data['id'] amount_refunded = data['amount_refunded'] reason = data.get('refund_reason') db.record_refund(charge_id, amount_refunded, reason) revoke_access_if_full_refund(data) 

App Store Server Notifications (iOS)

Apple использует JWT-подписанные уведомления версии 2. Верификация — через публичный ключ Apple (загружается из .well-known/apple-app-site-association или через AppleJWT библиотеку):

from appstoreconnect import AppStoreServerNotificationsClient @app.post("/webhooks/apple") async def apple_webhook(request: Request): body = await request.json() signed_payload = body.get("signedPayload") client = AppStoreServerNotificationsClient() try: notification = client.decode_notification(signed_payload) except Exception: raise HTTPException(400) notification_type = notification.notificationType subtype = notification.subtype transaction_info = notification.data.signedTransactionInfo match notification_type: case "DID_RENEW": # Подписка продлена extend_ios_subscription(transaction_info) case "EXPIRED": # Подписка истекла (subtype: VOLUNTARY / BILLING_RETRY / PRICE_INCREASE) deactivate_ios_subscription(transaction_info, reason=subtype) case "REFUND": # Возврат через Apple handle_ios_refund(transaction_info) case "GRACE_PERIOD_EXPIRED": # Истёк grace period — окончательно блокируем hard_deactivate_ios_subscription(transaction_info) 

Google Play Real-time Developer Notifications (Android)

Google Play отправляет уведомления через Cloud Pub/Sub, не через HTTP webhook. Нужно создать тему Pub/Sub, подписку, и поллить или использовать push-подписку:

from google.cloud import pubsub_v1 import base64 def process_pubsub_message(message: pubsub_v1.types.ReceivedMessage): data = json.loads(base64.b64decode(message.message.data)) if 'subscriptionNotification' in data: notification = data['subscriptionNotification'] notification_type = notification['notificationType'] purchase_token = notification['purchaseToken'] # Верифицируем через Google Play Developer API purchase = google_play_client.purchases().subscriptions().get( packageName=PACKAGE_NAME, subscriptionId=notification['subscriptionId'], token=purchase_token ).execute() match notification_type: case 4: # SUBSCRIPTION_PURCHASED activate_android_subscription(purchase_token, purchase) case 2: # SUBSCRIPTION_RENEWED extend_android_subscription(purchase_token, purchase) case 3: # SUBSCRIPTION_CANCELED mark_android_subscription_cancelled(purchase_token) case 13: # SUBSCRIPTION_EXPIRED deactivate_android_subscription(purchase_token) 

Сравнение обработчиков webhook разных сторах

Параметр Stripe App Store Google Play
Протокол HTTP POST HTTP POST (JWT) Pub/Sub (push/pull)
Идемпотентность event.id signedPayload JTI messageId Pub/Sub
Таймаут ответа 30 секунд 5 секунд 10 секунд
Повторы Да, до 3 дней Да, до 24 часов Да, до 7 дней
Верификация stripe-signature + secret Apple JWT key Google API проверка
Типичные события payment_succeeded, refund DID_RENEW, EXPIRED, REFUND SUBSCRIPTION_RENEWED, CANCELED

Типичные ошибки при реализации webhook

Ошибка Последствие Решение
Отсутствие идемпотентности Двойное продление подписки Хранить event.id в Redis
Нет верификации подписи Бесплатный доступ злоумышленнику Проверять подпись каждого события
Отправка 200 до завершения обработки Потеря события при сбое Асинхронная обработка в фоне
Блокировка по EXPIRED без ожидания Ложное отключение доступа Использовать grace-период
Детали реализации идемпотентности

Храните обработанные ID в Redis с TTL 30 дней. Для особо критичных случаев добавьте блокировку на уровне базы: перед обработкой вставляйте event_id в таблицу processed_events с unique constraint. При попытке вставить дубликат — ловите исключение и пропускайте.

Что входит в разработку webhook под ключ

  • Проектирование схемы событий и эндпоинтов для каждого стора.
  • Реализация идемпотентности (Redis/Postgres) и верификации подписи.
  • Обработка renewal (продление), cancel (отмена), refund (возврат), upgrade/downgrade.
  • Обработка граничных случаев: повторная активация, grace period, retry payment.
  • Логирование всех событий, мониторинг ошибок, алерты при сбоях.
  • Интеграционное тестирование с песочницей каждого стора (TestFlight, internal test track).
  • Документация API webhook и инструкция по развертыванию.
  • Поддержка после внедрения (1 месяц).

Порядок событий: иногда cancel приходит раньше renewal

Особенность App Store: EXPIRED может прийти за секунды до DID_RENEW при успешном продлении в последний момент. Если заблокировали доступ по EXPIRED — нужно немедленно восстановить по DID_RENEW. Состояние подписки в базе должно определяться не только webhook-событиями, но и верификацией receipt/purchase на сервере Apple/Google. У нас есть 5+ лет опыта в интеграции платежей — мы учитываем все нюансы. Экономия времени на отладку таких граничных случаев составляет до 50%.

Сроки

3–5 дней. Stripe webhook с идемпотентностью и полным набором событий — 1,5 дня. App Store Server Notifications v2 — 1 день. Google Play Pub/Sub — 1 день. Интеграционное тестирование всех сценариев — 0,5–1 день. Стоимость рассчитывается индивидуально. Свяжитесь — мы оценим ваш проект и предложим оптимальное решение. Обращайтесь к нам для внедрения надёжного webhook-обработчика.

Для глубокого понимания работы webhook рекомендуем официальную документацию Stripe.