Серверний Webhook для підписок: renewal, cancel, refund

Бізнес за підпискою втрачає виручку, коли події renewal, cancel і refund обробляються ненадійно: дублюються, губляться або надходять у хаотичному порядку. Ми реалізуємо серверний webhook підписок під ключ — від Stripe, App Store і Google Play до вашої бази, із захистом від задвоєнь і збоїв. Наша команда забезпечує стабільну обробку платежів і подальшу підтримку, щоб ви не втрачали гроші на помилках інтеграції.

Розробка та підтримка будь-яких видів мобільних додатків:

Інформаційні та розважальні мобільні програми
Новинки, ігри, довідники, онлайн-каталоги, погодні, фітнес та здоров'я, туристичні, освітні, соціальні мережі та месенджери, квіз, блоги та подкасти, форуми, агрегатори
Мобільні програми електронної комерції
Інтернет-магазини, B2B-додатки, маркетплейси, онлайн-обмінники, кешбек-сервіси, біржі, дропшиппінг-платформи, програми лояльності, доставка їжі та товарів, платіжні системи
Мобільні програми для управління бізнес-процесами
CRM-системи, ERP-системи, управління проектами, інструменти для команди продажів, облік фінансів, управління виробництвом, логістика та доставка, управління персоналом, системи моніторингу даних
Мобільні програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, платформи надання електронних послуг, платформи кешбеку, відеохостинги, тематичні портали, платформи онлайн-бронювання та запису, платформи онлайн-торгівлі

Це лише деякі з типів мобільних додатків, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

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

Реалізація серверного 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.

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

Останні роботи

  • Розробка мобільного додатка для компанії FEEDME
    Розробка мобільного додатка для компанії FEEDME
    1010
  • Розробка мобільного додатку для компанії XOOMER
    Розробка мобільного додатку для компанії XOOMER
    903
  • Розробка мобільного додатку для компанії RHL
    Розробка мобільного додатку для компанії RHL
    1332
  • Розробка мобільного додатку для компанії ZIPPY
    Розробка мобільного додатку для компанії ZIPPY
    1180
  • Розробка мобільного додатку для компанії Affhome
    Розробка мобільного додатку для компанії Affhome
    1107
  • Розробка мобільного додатку для компанії FLAVORS
    Розробка мобільного додатку для компанії FLAVORS
    714