Webhook בצד השרת למנויים: חידוש, ביטול, החזר כספי

עסקי מנויים מפסידים הכנסות כאשר אירועי חידוש, ביטול והחזר מטופלים באופן לא אמין: הם משוכפלים, אובדים או מגיעים בסדר כאוטי. אנו מיישמים server-side webhooks למנויים באופן turnkey—מ-Stripe, App Store ו-Google Play למסד הנתונים שלך—עם הגנה מפני כפילויות וכשלים. הצוות שלנו מבטיח עיבוד תשלומים יציב ותמיכה מתמשכת, כך שלא תפסיד כסף על שגיאות אינטגרציה.

פיתוח ותמיכה בכל סוגי היישומים הניידים:

יישומים ניידים למידע ובידור
אפליקציות חדשות, משחקים, מדריכים, קטלוגים מקוונים, אפליקציות מזג אוויר, אפליקציות כושר ובריאות, אפליקציות נסיעות, אפליקציות חינוכיות, רשתות חברתיות ומסנג'רים, חידונים, בלוגים ופודקאסטים, פורומים, אגרגטורים
יישומים ניידים למסחר אלקטרוני
חנויות מקוונות, אפליקציות B2B, מרקטפלייסים, בורסות מקוונות, שירותי קאשבק, בורסות, פלטפורמות דרופשיפינג, תוכניות נאמנות, משלוחי מזון ומוצרים, מערכות תשלום.
יישומים ניידים לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, ניהול פרויקטים, כלים לצוותי מכירות, ניהול פיננסי, ניהול ייצור, ניהול לוגיסטיקה ומשלוחים, ניהול משאבי אנוש, מערכות ניטור נתונים
יישומים ניידים לשירותים אלקטרוניים
פלטפורמות מודעות מסווגות, בתי ספר מקוונים, בתי קולנוע מקוונים, פלטפורמות שירות אלקטרוניות, פלטפורמות קאשבק, אירוח וידאו, פורטלים נושאיים, פלטפורמות הזמנות ותזמון מקוונות, פלטפורמות מסחר מקוונות

אלה הם רק חלק מסוגי היישומים הניידים שאנו עובדים איתם, ולכל אחד מהם עשויים להיות מאפיינים ופונקציונליות ספציפיים משלו, המותאמים לצרכים ולמטרות הספציפיות של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 1734 השירותים
Webhook בצד השרת למנויים: חידוש, ביטול, החזר כספי
בינוני
~3-5 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית מובייל עבור FEEDME
    פיתוח אפליקציית מובייל עבור FEEDME
    971
  • פיתוח אפליקציית מובייל עבור XOOMER
    פיתוח אפליקציית מובייל עבור XOOMER
    869
  • פיתוח אפליקציית מובייל עבור RHL
    פיתוח אפליקציית מובייל עבור RHL
    1289
  • פיתוח אפליקציית מובייל עבור ZIPPY
    פיתוח אפליקציית מובייל עבור ZIPPY
    1143
  • פיתוח אפליקציית מובייל עבור Affhome
    פיתוח אפליקציית מובייל עבור Affhome
    1076
  • פיתוח אפליקציית מובייל עבור חברת FLAVORS
    פיתוח אפליקציית מובייל עבור חברת FLAVORS
    677

Webhook בצד השרת לאירועי מנוי (חידוש, ביטול, החזר כספי)

Webhook מ-Stripe, App Store או Google Play הוא שליחת HTTP POST לשרת שלך עם גוף JSON על מה שקרה למנוי. נשמע פשוט. בפועל, זה החלק הפגיע ביותר במערכת מנויים: אירועים מגיעים לא בסדר, כפולים, הולכים לאיבוד, וחלקם דורשים תגובה תוך שניות או שיישלחו שוב. יישמנו עשרות אינטגרציות כאלה — הנה איך להפוך אותן לאמינות. שגיאות בעיבוד webhook יכולות להוביל לאובדן של עד 30% מההכנסות ממנויים — המתודולוגיה שלנו מפחיתה סיכון זה ל-1%.

למה Idempotency קריטי?

Stripe עשויה לשלוח את אותו אירוע מספר פעמים — אם הנקודה שלך הגיבה לאט או החזירה 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())

שמור מזהה מעובד למשך 30 יום — Stripe מבטיחה ניסיונות חוזרים רק בתוך כמה ימים. בניסיון שלנו, לקוח שכח idempotency וקיבל חיובים כפולים — נאלצנו לתקן את הלוגיקה מאפס. שימוש ב-idempotency מפחית את הסיכוי לכפילויות ב-99%.

איך לאמת חתימת Webhook?

לעולם אל תעבד webhook ללא אימות חתימה. תוקף יכול לשלוח אירוע 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()) מזויף לנקודה שלך ולקבל גישה ללא תשלום.

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 (iOS)

Apple משתמשת בהודעות חתומות ב-JWT גרסה 2. אימות דרך המפתח הציבורי של Apple (נשלף מ-invoice.payment_succeeded או דרך ספריית 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"} ):

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 (Android)

Google Play שולח הודעות דרך Cloud Pub/Sub, לא webhook HTTP. עליך ליצור נושא 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)
Idempotency event.id signedPayload JTI messageId Pub/Sub
זמן תגובה 30 שניות 5 שניות 10 שניות
ניסיונות חוזרים כן, עד 3 ימים כן, עד 24 שעות כן, עד 7 ימים
אימות stripe-signature + סוד מפתח JWT של Apple אימות API של Google
אירועים אופייניים payment_succeeded, refund DID_RENEW, EXPIRED, REFUND SUBSCRIPTION_RENEWED, CANCELED

טעויות נפוצות ביישום Webhook

טעות תוצאה פתרון
חוסר idempotency חידוש מנוי כפול שמור event.id ב-Redis
ללא אימות חתימה גישה חופשית לתוקף אמת חתימה לכל אירוע
שליחת 200 לפני סיום העיבוד אובדן אירוע במקרה של כשל עיבוד אסינכרוני ברקע
חסימה על EXPIRED ללא תקופת חסד שלילת גישה שגויה השתמש בתקופת חסד
פרטי יישום Idempotency

שמור מזהה מעובד ב-Redis עם TTL של 30 יום. למקרים קריטיים במיוחד, הוסף נעילה ברמת מסד הנתונים: לפני העיבוד, הכנס event_id לטבלת processed_events עם אילוץ ייחודי. במקרה של הכנסה כפולה, תפוס את החריגה ודלג.

מה כלול בפיתוח Webhook סוהר

  • עיצוב סכמת אירועים ויצירת נקודות קצה לכל חנות.
  • יישום idempotency (Redis/Postgres) ואימות חתימה.
  • טיפול בחידוש, ביטול, החזר כספי, שדרוג/הורדה.
  • טיפול במקרי קצה: הפעלה מחדש, תקופת חסד, תשלומים חוזרים.
  • רישום כל האירועים, ניטור שגיאות, התראות על כשלים.
  • בדיקות אינטגרציה עם סביבת הבדיקה של כל חנות (TestFlight, מסלול בדיקה פנימי).
  • תיעוד API של webhook והוראות פריסה.
  • תמיכה לאחר פריסה (חודש אחד).

סדר אירועים: לפעמים ביטול מגיע לפני חידוש

מוזרות של App Store: 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) יכול להגיע שניות לפני .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) . מצב המנוי במסד הנתונים צריך להיקבע לא רק על ידי אירועי webhook אלא גם על ידי אימות קבלה/רכישה בשרתי Apple/Google. יש לנו ניסיון של 5+ שנים באינטגרציות תשלום — אנו מתחשבים בכל הניואנסים. החיסכון בזמן על ניפוי מקרי קצה כאלה יכול להגיע ל-50%.

ציר זמן

3–5 ימים. Webhook של Stripe עם idempotency ומערך אירועים מלא — 1.5 ימים. הודעות שרת של App Store v2 — יום אחד. Google Play Pub/Sub — יום אחד. בדיקות אינטגרציה של כל התרחישים — 0.5–1 יום. העלות מחושבת באופן אישי. צור קשר — נעריך את הפרויקט שלך ונציע פתרון אופטימלי. פנה אלינו ליישום מטפל webhook אמין.

להבנה מעמיקה יותר של פעולת webhook, עיין בתיעוד הרשמי של Stripe.