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.