Webhooks
- Pradeep P
- 3 days ago
- 3 min read
Series: Modern System Design · Layer 2 — Communication
Layer 2 · Post 10 of 10
← Previous: Server-Sent Events
Layer 2 — Communication · Post 25 of 88
A webhook is an HTTP callback: your system tells another system "this happened" by POSTing to a URL they registered.
What you'll learn
How webhooks differ from polling an API
Signatures, retries, and why receivers must be idempotent
How to design them as a producer and how to consume them safely
The idea in one minute
Instead of Stripe asking every merchant "any new payments?" a thousand times a second, you give Stripe a URL. When a payment succeeds, Stripe POSTs JSON to that URL.
That POST is a webhook: an event delivered as an inbound HTTP request you do not initiate.
It is pub/sub where the transport is your customer's HTTPS endpoint. No shared Kafka. The internet is the bus.
Why it matters
Integrations between companies almost always become webhooks. GitHub, Stripe, Slack, Shopify, Twilio — same shape.
If you build a platform, you will send webhooks. If you glue SaaS together, you will receive them. Both sides have failure modes that look like Layer 3 (retries, timeouts, idempotency) because they are Layer 3 over HTTP.
How it works
Producer (you send)
Customer registers https://their-app.com/hooks/stripe.
Your worker (not the request path) POSTs { "type": "invoice.paid", "id": "evt_..." }.
You want 2xx. Anything else, retry with backoff.
Sign the body (Stripe-Signature) so they can prove it was you.
Give them a delivery log and a way to replay.
Do this from a queue, not from the original user request. Their server might be slow. You should not stall checkout on their webhook.
Consumer (you receive)
Verify the signature before trusting the JSON.
Return 200 quickly. Do heavy work async (your own queue). If you 500, they will retry and you may double-process.
Deduplicate on event id.
Treat the payload as untrusted input.
At-least-once, again
Producers retry. You will see the same event twice. Idempotency keys / event ids are mandatory.
A simple example
Your app sells subscriptions.
You receive invoice.paid from Stripe.
You verify the signature, enqueue activate(userId), return 200.
A worker grants access. If it already granted that evt_123, it no-ops.
You also send member.upgraded to enterprise customers' URLs so their IT systems provision seats. Same rules in reverse: queue, sign, retry, DLQ for URLs that have been 500 for a week.
Common mistakes
Processing in the HTTP handler for 30 seconds. Timeouts, duplicate deliveries, angry producers.
No signature check. Anyone who finds the URL can fake invoice.paid.
200 on garbage. You ack and drop a malformed event. Prefer 4xx for invalid signatures (don't retry forever) and 5xx only for "try again."
HTTP instead of HTTPS. Tokens and PII on the wire.
Assuming order. updated can arrive before created if you retry. Design receivers to upsert.
How this shows up in real systems
Stripe Dashboard → Webhooks: the textbook.
GitHub Apps, Slack events, Shopify webhooks.
Internal "webhooks" between services: often better as a queue. Webhooks shine at trust boundaries (other companies, customer infra).
Layer 3 will make retries and idempotency precise. You already need both to ship webhooks.
Recap
A webhook is their URL, your POST, when a fact happens.
Sign, queue, retry, idempotent receive.
Fast 2xx; slow work off the request.
Layer 2 was how components talk. Layer 3 is how those talks fail and recover.
Series: Modern System Design · Layer 2 — Communication
Layer 2 · Post 10 of 10
← Previous: Server-Sent Events



Comments