Webhooks: when the API calls you
Every request so far in this track went from your code to somebody's API. A
webhook reverses the arrow: you register a URL, and the API POSTs to it when something happens.
That single reversal changes every assumption you have been working under.
| Polling | Webhook |
|---|---|
| You are the client | You are the server |
| You choose when to ask | You are called whenever, including 3 a.m. |
| A failure is yours to retry | A failure is theirs to retry — at you |
| You authenticate to them | They must authenticate to you |
At-least-once is the only guarantee on offer
Look at what happened in that run: a failure, a silence, a request for more time, and finally an acceptance. Each attempt was re-signed, and each carried the same event id.
The silence is the interesting one. When the sender times out, it cannot distinguish "the receiver never got it" from "the receiver processed it and the reply was lost". So it retries — and the receiver may well execute the same event twice.
There is no protocol fix for this. Exactly-once delivery is not available; exactly-once processing is, and you build it by making your handler
idempotent, keyed on the sender's event id and never on anything you compute yourself.
Signatures, and verifying before you parse
Your endpoint is a public URL, so anyone can POST to it. The signature is what separates a real event from a forgery, and the details matter:
signature = HMAC(secret, timestamp + "." + raw_body)The timestamp is inside the signed material, so an old capture cannot be replayed with a fresh timestamp bolted on. Every retry is signed afresh while the event id stays constant — which is exactly what makes deduplication possible.
Two implementation rules that are easy to get wrong: compare in constant time, or the comparison itself leaks the correct signature one byte at a time; and verify before parsing, since a parser is a much larger attack surface than an HMAC comparison.
Key takeaways
- A webhook reverses the roles: your endpoint is the server, and it must be public, fast and always available.
- A timeout is indistinguishable from a lost reply, so at-least-once is the only delivery guarantee available.
- Exactly-once processing is yours to build, by keying an idempotent handler on the sender’s event id.
- The signature covers timestamp and raw body together, which is what stops an old capture being replayed.
- Verify in constant time before parsing, acknowledge immediately, and do the real work on a queue.