A webhook is an HTTP callback — a POST request that a provider sends to your server when something happens. Understanding the full delivery lifecycle helps you build reliable integrations that handle failures gracefully.
Step 1: You Register a URL
Every webhook starts with registration. You give the provider a URL — your endpoint — and tell it which events to send. Stripe calls this a Webhook Endpoint. GitHub calls it a webhook. Regardless of the name, the mechanism is the same: you're telling the provider 'when X happens, POST to this URL.'
Step 2: An Event Occurs
When the event happens on the provider's side — a payment succeeds, a user signs up, a build completes — the provider constructs a JSON payload describing what happened.
{
"id": "evt_3PxK2L...",
"type": "payment_intent.succeeded",
"created": 1714000000,
"data": { "object": { "amount": 4999, "currency": "usd", "status": "succeeded" } }
}Step 3: HTTP POST Delivery
The provider makes an HTTP POST to your URL. Your server must respond with a 2xx status within the provider's timeout window (typically 5-30 seconds). A timeout or non-2xx response is treated as a delivery failure.
Step 4: Signature Verification
Signature verification proves the request came from the provider. The provider signs the raw request body using HMAC-SHA256 with a shared secret. Critical: always verify against the raw bytes of the request body, before any JSON parsing.
import hmac, hashlib
def verify(raw_body: bytes, sig_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig_header)Step 5: Respond Fast, Process Async
Return 200 OK immediately. Heavy processing should happen asynchronously after you've acknowledged receipt. This prevents timeouts.
Step 6: Retry Logic
If your server returns a non-2xx response or times out, the provider retries. Stripe retries over 3 days; GitHub retries up to 10 times; Shopify retries 19 times over 48 hours.
Step 7: Idempotency
Because providers retry, your handler may receive the same event more than once. Make your handler idempotent — store event IDs and check before processing.
INSERT INTO processed_events (event_id, processed_at)
VALUES ($1, now())
ON CONFLICT (event_id) DO NOTHING;Debugging Webhooks
Use WebhookWhisper to capture live webhook traffic with full request details, replay events, and inspect signatures without needing to deploy code.