Named by 9 of 26 clients, always in the same breath as retries
Send it something
it should refuse.
The screening question is “explain how you handle webhook validation, API calls, retries and safe failure escalation”. Three things decide it: whether the signature covers what you think it covers, whether a captured request expires, and what happens when the same delivery arrives twice.
Your browser signs each of these with the published secret and sends it to a real endpoint. Every refusal names which check stopped it.
Your browser signs each one with the published secret, so these are genuine signatures rather than prepared ones. Send the correct delivery first, then the repeat becomes available.
The secret is printed here on purpose
secret demonstration-secret-not-a-real-one
signature sha256 hmac over `${timestamp}.${rawBody}`, hex
headers X-Signature, X-Timestamp, X-Idempotency-Key
window 300 seconds either sideThis endpoint records that a request arrived and does nothing else, so a published secret costs nothing, and it is the only way a stranger can sign a request and watch the checks work. A real integration takes its secret from the sender and keeps it in an environment variable, which is the boring answer and the correct one. The endpoint describes itself and shows the last twenty attempts from everybody.
Five decisions, and the classic mistake each one avoids
The signature covers the raw body, read once
A receiver that parses the JSON and re-serialises it before checking is comparing a different string from the one that was signed. Key order, whitespace and number formatting survive that round trip by luck, so when it works it is a coincidence rather than a check. Press "a tampered body" above: correctly signed, one number changed afterwards, refused.
The timestamp is inside the signed material
Sign the body alone and a captured request can be replayed forever with a fresh timestamp header, because the header is not covered and the receiver cannot tell. The button that tries exactly that is the sixth one, and it is refused as a bad signature rather than as a stale request, which is the tell that the timestamp is genuinely part of what was signed.
The window looks both ways
Five minutes either side. Checking only that a request is not too old accepts anything dated far in the future, which is a replay that never expires. Checking only the future direction is worse. Both edges are tested, and a small clock skew forwards is tolerated because two servers never agree exactly.
A repeat is a success, not an error
Every sender retries. Answering 4xx to a retry makes a well behaved sender retry harder, which is the opposite of what anybody wants. So a delivery with a key that has been seen answers 200, says when it first arrived, and does nothing a second time. The key lives in a table rather than in memory, because an idempotency store that forgets on every deploy is a guarantee that is really a coincidence.
Compared in constant time, and cheap checks first
A plain equals returns as soon as two bytes differ, and that timing is measurable over enough requests. It is a slow attack and a real one, and the fix is one function call. The age of the request is checked before the signature is computed, so a flood of stale deliveries costs a comparison each rather than an HMAC each.
What this is not
There is no queue behind it. A real receiver acknowledges quickly and does the work somewhere else, because a sender that times out retries and a slow handler turns one delivery into five. This one writes a row and answers, which is fast enough to hide the problem rather than solve it.
One secret, not rotated. A real integration keeps the previous secret valid for a window so a rotation does not drop deliveries, and that is not shown here.
25 tests cover the decisions with no network in them, including both edges of the window and the captured-signature-with-a-fresh-timestamp case.
The whole list is 41 requirements taken from 114 job posts, with the gaps shown at the same size as the wins.