Skip to content

Stripe Webhook Events: Real-Time Payment & Subscription Notifications

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Stripe Webhook Events: Real. We cover key concepts, practical examples, and best practices to help you master this topic.

Stripe Webhooks send HTTP POST requests to your server when events happen — payments succeed, subscriptions update, invoices fail. Processing these reliably is essential for order fulfillment.

What You'll Learn

How to receive and verify Stripe webhooks, Process key events (checkout.session.completed, invoice events, subscription events), handle idempotency, and build event-driven fulfillment.

Why It Matters

Webhooks are Stripe's way of telling your server what happened. Without them, you won't know if a payment succeeded or failed. DodaTech processes 50+ webhook event types to keep billing synchronized.

Real-World Use

A customer completes checkout. Stripe sends checkout.session.completed webhook. Your handler verifies the signature, activates the subscription, sends a welcome email, and records the Transaction.

flowchart LR
    A["Stripe\nEvent Occurs"] --> B["HTTP POST\nto Your Endpoint"]
    B --> C["Verify\nSignature"]
    C --> D{"Event\nType?"}
    D -->|"checkout.session.completed"| E["Activate\nSubscription"]
    D -->|"invoice.payment_failed"| F["Notify\nCustomer"]
    D -->|"customer.subscription.deleted"| G["Downgrade\nAccount"]
    style C fill:#fef3c7,stroke:#d97706
    style D fill:#dbeafe,stroke:#2563eb

Webhook Handler

from flask import Flask, request, jsonify
import stripe
import os

app = Flask(__name__)
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
endpoint_secret = os.environ["STRIPE_WEBHOOK_SECRET"]

@app.route("/stripe/webhook", methods=["POST"])
def stripe_webhook():
    payload = request.get_data(as_text=True)
    sig_header = request.headers.get("Stripe-Signature")

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, endpoint_secret
        )
    except ValueError:
        return jsonify({"error": "Invalid payload"}), 400
    except stripe.error.SignatureVerificationError:
        return jsonify({"error": "Invalid signature"}), 400

    event_type = event["type"]
    data = event["data"]["object"]

    print(f"Received event: {event_type}")

    handlers = {
        "checkout.session.completed": handle_checkout_completed,
        "payment_intent.succeeded": handle_payment_succeeded,
        "payment_intent.payment_failed": handle_payment_failed,
        "customer.subscription.updated": handle_subscription_updated,
        "customer.subscription.deleted": handle_subscription_deleted,
        "invoice.payment_succeeded": handle_invoice_paid,
        "invoice.payment_failed": handle_invoice_failed,
        "charge.dispute.created": handle_dispute,
    }

    handler = handlers.get(event_type)
    if handler:
        handler(data)
    else:
        print(f"Unhandled event: {event_type}")

    return jsonify({"status": "ok"}), 200

Event Handlers

def handle_checkout_completed(session):
    customer_email = session.get("customer_details", {}).get("email")
    subscription_id = session.get("subscription")
    metadata = session.get("metadata", {})

    print(f"Checkout completed for {customer_email}")
    print(f"Subscription: {subscription_id}")

    if subscription_id:
        activate_subscription(customer_email, subscription_id)
    else:
        fulfill_one_time_order(session)

def handle_subscription_updated(subscription):
    status = subscription["status"]
    customer = subscription["customer"]

    print(f"Subscription {subscription['id']}: {status}")

    if status == "active":
        update_user_subscription(customer, subscription["id"], "active")
    elif status == "past_due":
        notify_payment_failed(customer)

def handle_invoice_failed(invoice):
    customer_email = invoice.get("customer_email")
    amount_due = invoice["amount_due"] / 100
    attempt_count = invoice["attempt_count"]

    print(f"Invoice payment failed for {customer_email}")
    print(f"Amount: ${amount_due}, Attempt: {attempt_count}")
    send_payment_failure_email(customer_email, attempt_count)

Testing Webhooks Locally

# Install Stripe CLI
# brew install stripe/stripe-cli/stripe

# Listen and forward to local server
stripe listen --forward-to localhost:5000/stripe/webhook

# Trigger test events
stripe trigger checkout.session.completed
stripe trigger payment_intent.succeeded
stripe trigger customer.subscription.updated

# Expected output:
# Got event: checkout.session.completed (session: cs_test_...)
# Forwarded to http://localhost:5000/stripe/webhook
# Got event: payment_intent.succeeded (intent: pi_test_...)

Common Mistakes

1. Not Verifying Signatures

Without signature verification, anyone can POST fake events. Always verify using stripe.Webhook.construct_event() with your endpoint secret.

2. Processing Events Synchronously

Slow processing causes Stripe to retry (timeout after 30 seconds). Acknowledge with 200 immediately, process asynchronously (queue).

3. Not Handling Event Idempotency

Stripe may deliver the same event multiple times. Use the event ID for deduplication. Stripe also provides idempotency keys in the request.

4. Ignoring Webhook Delivery Failures

If your endpoint returns non-200, Stripe retries for up to 3 days. Check the Stripe Dashboard > Webhooks > Failed attempts regularly.

5. Mixing Test and Live Webhooks

Test mode events go to test endpoints, live events to live. Configure separate endpoints or check the livemode field in the event payload.

Practice Questions

  1. How do you verify a webhook signature?
  2. What is the most reliable way to fulfill orders after payment?
  3. How do you handle duplicate webhook events?
  4. What events should you listen for subscription management?

Answers:

  1. Use stripe.Webhook.construct_event(payload, sig_header, secret). It validates the HMAC signature and returns the verified event.
  2. Use the checkout.session.completed webhook event. It's the most reliable indicator that payment was collected.
  3. Store processed event IDs (from event.id). Check before processing. Stripe also includes an idempotency key (Idempotency-Key header).
  4. customer.subscription.updated, customer.subscription.deleted, invoice.payment_succeeded, invoice.payment_failed.

Challenge: Build a complete webhook handler: verify signature, dispatch to type-specific handlers, implement event deduplication, process checkout completions (activate subscription), handle invoice failures (notify customer), and log all events with timestamps.

FAQ

How reliable is Stripe webhook delivery?

Stripe retries failed deliveries for up to 3 days with exponential backoff. Events have guaranteed at-least-once delivery.

How do I test webhooks locally?

Use the Stripe CLI: stripe listen --forward-to localhost:5000/stripe/webhook and trigger events with stripe trigger.

What is the Stripe-Signature header?

It's an HMAC-SHA256 signature of the payload + timestamp, signed with your webhook signing secret. Used for verification.

How should I structure webhook code?

Use an event-type dispatcher pattern. Map event types to handler functions. Separate webhook verification from business logic.

Can I receive webhooks for multiple environments?

Use separate endpoints for test and live, or use a single endpoint and check the livemode field to differentiate.

Mini Project

Build a robust webhook system: local Stripe CLI forwarding, signature verification, event dispatcher with 10+ handler functions, event deduplication (store processed event IDs), async processing with a task queue, and a monitoring dashboard showing webhook health.

What's Next

Customer Portal — let customers manage their billing without contacting support.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro