Skip to content
RungsySign in

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
    end
Diagram source for Webhooks.

What 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?

Sources

Keep track of this

Add Asynchronous Integration to your map and Rungsy will schedule reviews so you actually remember it.