Server-Side Webhook for Subscriptions: Renewal, Cancel, Refund

Server-Side Webhook for Subscription Events (Renewal, Cancel, Refund) Webhook from Stripe, App Store, or Google Play is an HTTP POST to your server with a JSON body about what happened to the subscription. Sounds simple. In practice, it is the most vulnerable part of a subscription system: events

Development and support of all types of mobile applications:

Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Showing 1 of 1All 1734 services
Server-Side Webhook for Subscriptions: Renewal, Cancel, Refund
Medium
~3-5 days

Our competencies:

Frequently Asked Questions

Latest works

  • image_mobile-applications_feedme_467_0.webp
    Development of a mobile application for FEEDME
    896
  • image_mobile-applications_xoomer_471_0.webp
    Development of a mobile application for XOOMER
    782
  • image_mobile-applications_rhl_428_0.webp
    Development of a mobile application for RHL
    1216
  • image_mobile-applications_zippy_411_0.webp
    Development of a mobile application for ZIPPY
    1079
  • image_mobile-applications_affhome_429_0.webp
    Development of a mobile application for Affhome
    1003
  • image_mobile-applications_flavors_409_0.webp
    Development of a mobile application for the FLAVORS company
    597

Server-Side Webhook for Subscription Events (Renewal, Cancel, Refund)

Webhook from Stripe, App Store, or Google Play is an HTTP POST to your server with a JSON body about what happened to the subscription. Sounds simple. In practice, it is the most vulnerable part of a subscription system: events arrive out of order, duplicate, get lost, and some require a response within seconds or will be retried. We have implemented dozens of such integrations — here is how to make them reliable. Errors in webhook processing can lead to losing up to 30% of subscription revenue — our methodology reduces that risk to 1%.

Why Idempotency is Critical?

Stripe may send the same event multiple times — if your endpoint responded slowly or returned a 500. Processing invoice.payment_succeeded twice means extending the subscription twice in the database and sending two "thank you for payment" emails. Solution: store processed event.id:

def handle_stripe_webhook(event_id: str, event_type: str, event_data: dict): # Check idempotency if db.is_event_processed(event_id): return # already processed, do nothing # Process process_event(event_type, event_data) # Mark as processed db.mark_event_processed(event_id, processed_at=datetime.utcnow()) 

Store processed IDs for 30 days — Stripe guarantees retries only within a few days. In our practice, a client forgot idempotency and got double charges — we had to fix the logic from scratch. Using idempotency reduces the chance of duplicates by 99%.

How to Verify Webhook Signature?

Never process a webhook without verifying its signature. An attacker could send a fake invoice.payment_succeeded event to your endpoint and gain access without payment.

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)) # Process asynchronously — return 200 immediately background_tasks.add_task(process_stripe_event, event) return {"status": "ok"} 

Important: return HTTP 200 immediately, before processing finishes. Stripe considers a webhook failed if no response is received within 30 seconds. Processing should happen in the background.

Event Map: What to Do for Each

async def process_stripe_event(event: dict): event_type = event['type'] data = event['data']['object'] match event_type: case 'invoice.payment_succeeded': # Subscription renewed — update access period 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': # Handled by retry logic handle_payment_failure(data['subscription'], data.get('last_payment_error')) case 'customer.subscription.deleted': # Cancel: user canceled or retries exhausted 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': # Plan change, period change, reactivation if data['status'] == 'active' and data.get('pause_collection') is None: db.reactivate_subscription_if_paused(data['id']) case 'charge.refunded': # Refund 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 uses JWT-signed notifications version 2. Verification via Apple's public key (fetched from .well-known/apple-app-site-association or via AppleJWT library):

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": # Subscription renewed extend_ios_subscription(transaction_info) case "EXPIRED": # Subscription expired (subtype: VOLUNTARY / BILLING_RETRY / PRICE_INCREASE) deactivate_ios_subscription(transaction_info, reason=subtype) case "REFUND": # Refund through Apple handle_ios_refund(transaction_info) case "GRACE_PERIOD_EXPIRED": # Grace period expired — final block hard_deactivate_ios_subscription(transaction_info) 

Google Play Real-time Developer Notifications (Android)

Google Play sends notifications via Cloud Pub/Sub, not HTTP webhook. You need to create a Pub/Sub topic, subscription, and either poll or use push subscription:

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'] # Verify through 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) 

Comparison of Webhook Handlers Across Stores

Parameter Stripe App Store Google Play
Protocol HTTP POST HTTP POST (JWT) Pub/Sub (push/pull)
Idempotency event.id signedPayload JTI messageId Pub/Sub
Response timeout 30 seconds 5 seconds 10 seconds
Retries Yes, up to 3 days Yes, up to 24 hours Yes, up to 7 days
Verification stripe-signature + secret Apple JWT key Google API verification
Typical events payment_succeeded, refund DID_RENEW, EXPIRED, REFUND SUBSCRIPTION_RENEWED, CANCELED

Common Webhook Implementation Mistakes

Mistake Consequence Solution
Missing idempotency Double subscription renewal Store event.id in Redis
No signature verification Free access for attacker Verify signature for each event
Sending 200 before processing finishes Event loss on failure Async processing in background
Blocking on EXPIRED without grace period False access revoke Use grace period
Idempotency Implementation Details

Store processed IDs in Redis with a 30-day TTL. For highly critical cases, add a database-level lock: before processing, insert event_id into processed_events table with a unique constraint. On duplicate insert, catch the exception and skip.

What's Included in Turnkey Webhook Development

  • Event schema design and endpoint creation for each store.
  • Implementation of idempotency (Redis/Postgres) and signature verification.
  • Handling of renewal, cancel, refund, upgrade/downgrade.
  • Handling edge cases: reactivation, grace period, retry payments.
  • Logging all events, error monitoring, alerts for failures.
  • Integration testing with each store's sandbox (TestFlight, internal test track).
  • Webhook API documentation and deployment instructions.
  • Post-deployment support (1 month).

Event Order: Sometimes Cancel Arrives Before Renewal

App Store peculiarity: EXPIRED can arrive seconds before DID_RENEW when a renewal succeeds at the last moment. If you blocked access due to EXPIRED, you need to restore it immediately upon DID_RENEW. The subscription state in the database should be determined not only by webhook events but also by receipt/purchase verification on Apple/Google servers. We have 5+ years of payment integration experience — we account for all nuances. Time saved on debugging such edge cases can be up to 50%.

Timeline

3–5 days. Stripe webhook with idempotency and full event set — 1.5 days. App Store Server Notifications v2 — 1 day. Google Play Pub/Sub — 1 day. Integration testing of all scenarios — 0.5–1 day. The cost is calculated individually. Contact us — we will evaluate your project and propose the optimal solution. Reach out to us for implementing a reliable webhook handler.

For a deeper understanding of webhook operation, refer to Stripe's official documentation.