Skip to main content
Cloud & AI Hub
Browse
Glossary AI Directory Playgrounds Models Prompts Explainers Strategy Matrix Benchmark Decoder
beginner 6 mins Read

How Webhooks Event Notifications Work

How webhooks use HTTP callbacks to push real-time event data from services to your endpoints, and how to implement reliable delivery with signatures and retries.

Introduction

APIs are typically pull-based: your code calls a remote service to get data. Webhooks flip this model to push-based: the remote service calls your endpoint when something happens. Instead of polling Stripe every second to check if a payment succeeded, Stripe calls your /webhooks/stripe endpoint with the payment event the moment it occurs. This eliminates polling overhead, reduces latency, and decouples your system from external service polling cycles.

Step-by-Step: Webhook Lifecycle

flowchart TD
    A["1. Registration"]
    B["2. Event Delivery"]
    C["3. Signature Verification"]
    D["4. Idempotent Processing"]
    E["5. Reliable Delivery Patterns"]
    A --> B
    B --> C
    C --> D
    D --> E

Step 1: Registration

You register a callback URL with the provider β€” the URL they’ll POST events to:

# Stripe webhook registration via API
curl -X POST https://api.stripe.com/v1/webhook_endpoints   -u sk_live_xxx:   -d "url=https://api.yourapp.com/webhooks/stripe"   -d "enabled_events[]=payment_intent.succeeded"   -d "enabled_events[]=customer.subscription.deleted"

Step 2: Event Delivery

When the event occurs, the provider sends an HTTP POST to your endpoint with a JSON payload:

POST /webhooks/stripe HTTP/1.1
Host: api.yourapp.com
Content-Type: application/json
Stripe-Signature: t=1714123456,v1=abc123def456...

{
  "id": "evt_1234",
  "type": "payment_intent.succeeded",
  "data": {
    "object": {
      "id": "pi_abc123",
      "amount": 2000,
      "currency": "usd",
      "customer": "cus_xyz789"
    }
  }
}

Step 3: Signature Verification (Critical Security Step)

Anyone can POST to your webhook endpoint. Always verify the signature to confirm the event is genuinely from the provider:

import hmac, hashlib, time
from fastapi import Request, HTTPException

STRIPE_SECRET = "whsec_xxxxx"  # From Stripe dashboard

async def verify_stripe_webhook(request: Request):
    payload = await request.body()
    sig_header = request.headers.get("Stripe-Signature")
    
    # Parse timestamp and signature from header
    elements = {k: v for k, v in [e.split("=") for e in sig_header.split(",")]}
    timestamp = elements["t"]
    signature = elements["v1"]
    
    # Replay attack prevention: reject events older than 5 minutes
    if abs(time.time() - int(timestamp)) > 300:
        raise HTTPException(status_code=400, detail="Webhook timestamp too old")
    
    # Verify HMAC-SHA256 signature
    signed_payload = f"{timestamp}.{payload.decode()}"
    expected = hmac.new(STRIPE_SECRET.encode(), signed_payload.encode(), hashlib.sha256).hexdigest()
    
    if not hmac.compare_digest(expected, signature):
        raise HTTPException(status_code=400, detail="Invalid signature")
    
    return payload

Step 4: Idempotent Processing

Providers retry failed deliveries β€” your handler may receive the same event multiple times. Use the event ID for idempotency:

@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
    payload = await verify_stripe_webhook(request)
    event = json.loads(payload)
    
    # Idempotency check β€” skip if already processed
    event_id = event["id"]
    if await redis.exists(f"processed_event:{event_id}"):
        return {"status": "already_processed"}
    
    # Process the event
    if event["type"] == "payment_intent.succeeded":
        await fulfill_order(event["data"]["object"])
    
    # Mark as processed (TTL = 7 days, matching Stripe's retry window)
    await redis.setex(f"processed_event:{event_id}", 604800, "1")
    return {"status": "ok"}

Step 5: Reliable Delivery Patterns

Provider retry schedules vary:
  Stripe:  Retries over 72 hours with exponential backoff (8 attempts)
  GitHub:  Retries for 3 days (30+ attempts)
  Shopify: Retries 19 times over 48 hours

Endpoint requirements for reliable delivery:
  - Respond with HTTP 2xx within 5-30 seconds (or provider marks failed)
  - Return 200 immediately, process asynchronously via job queue
  - Never process in webhook handler itself (DB writes, API calls, emails)

Pattern:
  Webhook endpoint β†’ validate signature β†’ enqueue job β†’ return 200
  Job worker β†’ idempotency check β†’ process event β†’ mark done

Key Takeaways

  • Webhooks are push-based HTTP callbacks β€” the provider calls your endpoint when events occur, eliminating polling overhead.
  • Always verify the HMAC signature using your webhook secret before processing β€” any unsigned POST could be a spoofed attack.
  • Implement idempotency using the event ID β€” providers retry failed deliveries, and your handler will receive duplicates.
  • Respond with 200 immediately and process asynchronously via job queue β€” webhook endpoints that are slow get retried, creating duplicate processing risk.
  • Use replay attack prevention by rejecting events with timestamps older than 5 minutes (providers include a timestamp in the signature payload).

Historical figures, architectures, and capabilities are for informational purposes only. Not technical, professional, legal, or financial advice. Sources: Research papers, developer documentation.