Webhooks
Inverting the call — they tell you when something happened instead of you asking forever.
40 minDifficulty 2/5api · integration · eventsReviewedRead and accepted by a person.
Before this
Why this exists
You've integrated a payment provider. You need to know the moment a charge succeeds. So you poll: every 10 seconds, `GET /charges/123`. Ninety-nine times out of a hundred the answer is "still pending" — you've burned a hundred requests to learn nothing, and you still find out up to 10 seconds late.
The mental model
Polling is looking through the peephole every ten seconds to see if anyone's at the door. A webhook is a doorbell. You stop looking; they tell you.
How it works
It's just an HTTP request, pointed the other way
A webhook is not a special protocol. It's an ordinary HTTP POST — except *they* send it to *you*. You give the provider a URL you control. When something happens on their side, they POST a JSON body describing the event to that URL. Your server is now, for this one endpoint, playing the role of the server in someone else's client-server relationship. That's the whole inversion.
Anyone can POST to your URL
Your webhook endpoint is on the public internet. Anyone who learns the URL can send it a body that says `{ "event": "payment.succeeded", "amount": 100000 }`. HTTPS does not help here — it protects the data *in transit*, it says nothing about *who sent it*. So providers sign each request: they compute an HMAC of the raw body using a secret only the two of you know, and put it in a header. You recompute it and compare. No valid signature, no trust.
Delivery is at-least-once, so duplicates are normal
The provider needs to know you received the event. The only signal they have is your HTTP response. If your response is slow, or your server restarts mid-request, or the network drops the reply — they can't tell "lost request" from "lost response". So they retry. Which means **you will receive the same event more than once**, and this is not an error condition, it's the normal operation of the system. Every provider works this way, because the alternative — at-most-once — means silently losing events, which is worse.
Respond fast, work later
Providers time out aggressively — often 5 to 10 seconds — and a timeout counts as a failure, which triggers a retry. If your handler sends an email, updates three tables, and calls two other APIs before responding, you will blow the budget under load. Then you get retried, and now you're doing all that work twice, concurrently. The fix is structural: **verify, persist, respond 200, and do the work afterwards** — on a queue, in a background job, anywhere that isn't the request handler.
The mechanism
Five steps, in this order, every time. **1.** Read the *raw* body — not the parsed object, the exact bytes, because the signature was computed over those bytes. **2.** Recompute the HMAC with your shared secret and compare in constant time. **3.** Check the event id against a store of ones you've already handled; if it's there, return 200 and stop. **4.** Persist the event and return 200 immediately. **5.** Process it asynchronously.
sequenceDiagram
participant P as Provider
participant Y as Your endpoint
participant Q as Queue
participant W as Worker
P->>Y: POST /webhooks (body + signature)
Y->>Y: recompute HMAC of raw body
alt signature invalid
Y-->>P: 401 (no retry helps)
else already seen event id
Y-->>P: 200 (idempotent no-op)
else new + valid
Y->>Q: enqueue event
Y-->>P: 200 (under 1s)
Q->>W: deliver
W->>W: do the real work
endWhat people get wrong
- HTTPS means the request really came from the provider.
- HTTPS proves you're talking to the server named in the certificate. It proves nothing about who initiated an inbound request to you. Encryption in transit and sender authenticity are different problems. Only the signature solves the second one.
- Each event arrives exactly once, so I can process it directly.
- Delivery is at-least-once. Duplicates are guaranteed to happen eventually. The sender can't distinguish a lost request from a lost response, so it must retry, and retrying is how duplicates get created.
- Returning 500 on a bad signature tells the provider something is wrong.
- A 5xx means "retry me". A forged request will be retried forever. Return 4xx. Status codes are instructions to the retry mechanism, not just diagnostics.
- Comparing signatures with `===` is fine.
- Use a constant-time comparison like `crypto.timingSafeEqual`. `===` returns as soon as two bytes differ, so response time leaks how many leading bytes were correct — enough to reconstruct a valid signature over many attempts.
When not to use it
- You need sub-second, bidirectional updates to a specific logged-in user's browser.
- WebSockets or Server-Sent Events. Webhooks are server-to-server and one-directional.
- You control both sides and they're inside the same trust boundary.
- A message queue directly. Webhooks exist to cross an organisational boundary over HTTP; inside one, the HTTP hop is pure overhead.
- You need a guaranteed, ordered, complete event log you can replay from any point.
- An event streaming platform. Webhooks are best-effort and unordered.
Terms
- HMAC
- — A hash of a message combined with a secret key. Anyone with the key can compute it; nobody without the key can forge it.
- Idempotent
- — Safe to do more than once. Doing it five times leaves the system in the same state as doing it once.
- At-least-once delivery
- — A guarantee that a message will arrive, with no promise it arrives only once.
- Raw body
- — The exact bytes of the request before any JSON parsing. Signatures are computed over these, and re-serialising changes them.
- Replay attack
- — Capturing a genuine signed request and sending it again later. Defended against with a timestamp in the signed payload.
In an interview
How would you make a webhook handler reliable?
- verify the signature against the raw body, in constant time
- respond in under a second and move real work to a queue
- dedupe on the provider's event id — delivery is at-least-once
- 4xx for bad signatures so they aren't retried, 5xx only for your own transient failures
When would you use a webhook instead of polling?
- events are infrequent relative to how often you'd have to poll
- you need low latency without a tight poll interval
- you accept the cost: a public endpoint, signature verification, and duplicate handling
- polling is still better when you can't expose an endpoint or the provider is unreliable
Can you recall it?
In one or two sentences: what problem does a webhook solve that polling doesn't — and what new problem does it create?
Connected ideas
- Idempotency — Making 'do it again' safe — the single idea that separates reliable distributed code from hope.
- WebSockets — A connection that stays open both ways — real-time, at the cost of statefulness.
- Message Queues — Hand the work to someone else and answer now — the backbone of every responsive backend.
- Background Jobs — Retries, backoff, dead-letter queues — what happens after you say 202 Accepted.