A webhook is an inbound event sent by another service, usually after the original user request has ended. The receiving endpoint must not trust the JSON body simply because it came to a secret path. Verify a message authentication signature over the exact raw bytes and timestamp with the configured secret, apply the provider's documented timestamp rule, and reject unsupported event types. A correct signature proves the sender possessed a key; it does not prove the event is new or that every referenced record belongs to the local user. Store a stable event ID under a uniqueness constraint before applying side effects, so retries and replay produce one effective result. Keep key rotation and delivery failure handling in the contract.
Signed Webhook Delivery and Replay Control
Working case
An inspection vendor reports that media 61 finished processing for case 47. It sends event 584 with a signature and timestamp. The endpoint verifies the raw payload before JSON parsing, checks that the event is recent, then records event 584 and schedules a bounded case update. The vendor times out and sends the same event again. The second delivery receives a successful acknowledgement but does not attach the image twice. If the event references a case that belongs to another account, the signed origin does not override local record permission or mapping checks.
Implementation boundary
import { createHmac, timingSafeEqual } from "node:crypto";
function hasValidWebhookSignature(rawBody, timestampText, receivedHex, signingKey, nowSeconds) {
if (!/^\d+$/.test(timestampText) || !/^[a-f0-9]{64}$/i.test(receivedHex)) return false;
const sentAt = Number(timestampText);
if (!Number.isSafeInteger(sentAt) || Math.abs(nowSeconds - sentAt) > 300) return false;
const signedPayload = Buffer.concat([Buffer.from(`${timestampText}.`), rawBody]);
const expected = createHmac("sha256", signingKey).update(signedPayload).digest();
const received = Buffer.from(receivedHex, "hex");
return received.length === expected.length && timingSafeEqual(received, expected);
}Cost and boundaries
HMAC verification scans B body bytes in O(B) time before parsing. A uniqueness lookup for event IDs is normally indexed, but deduplication storage grows with the retention window. The receiver should acknowledge quickly and move longer work to a durable queue, creating an at-least-once worker path that also needs safe retries. Timestamp windows are a tradeoff: too wide allows more replay, too narrow rejects delayed but legitimate delivery. Observe delivery age and failure reasons without logging secrets or full private payloads.
Failure trace
The handler parses JSON, serializes it again, and signs the reconstructed text. Whitespace and key ordering change, so legitimate events fail. A second version compares signatures with an ordinary early-exit string comparison and does not track event IDs; retries cause duplicate notifications. Verify the original bytes, compare fixed-length digests safely, enforce freshness, and record event identity before the business effect. Do not label the snippet above a complete webhook handler: it shows only the signature comparison step.
Verification
- Alter one byte of a valid raw payload and require signature rejection.
- Deliver one signed event twice and assert one stored side effect with two safe acknowledgements.
- Send a correctly signed event outside the accepted timestamp window and reject its effect.
Decision note
A signed event is an input to local business rules, not a command that bypasses them. Validate the referenced case and event transition after signature and replay checks.
Common Mistakes
- Do not sign parsed and reserialized JSON instead of the raw bytes.
- Do not treat a valid signature as proof that an event is new.
- Do not perform long external work before safely recording the event.
Connected lessons
Integration and Verification; API Evolution and Compatibility Windows; Browser Journey and Fault-Injection Tests; Field and Lab Performance Evidence; Background Jobs and the Outbox Boundary; Idempotent Write Requests and Lost Responses; Authorization: check permission for this record on every request; Observability: connect user failure to a safe request trace.
Apply and check
Build Project: webhook contract and browser verification and review Web Development: identity and integration contracts quiz.
Further connections
Payment Webhook Reconciliation and Idempotent Fulfillment; Refunds, Reversals, and Payment Ledger State.
