How to Implement Secure Webhooks
Build secure webhooks: sign payloads with HMAC, verify signatures with constant-time comparison over the raw body, enforce idempotency by event id, retry with backoff, and block replays.
What and why
Webhooks let your service push events to other systems over HTTP instead of having them poll. Because the receiver runs code on data you send, webhooks must be authenticated, idempotent, and resilient to retries. This tutorial covers both the sender and the receiver.
Prerequisites
- A service that emits events.
- A receiver endpoint reachable over HTTPS.
- Basic understanding of HMAC.
Steps
1. Design the event payload
Include a stable event id, an event type, a timestamp, and the data:
{ "id": "evt_123", "type": "order.paid", "created": 1719446400, "data": { "orderId": "42" } }
2. Sign payloads with HMAC
The sender signs the raw body with a shared secret and sends the signature in a header:
const sig = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
// send header: X-Signature: t=<timestamp>,v1=<sig>
Sign the exact bytes sent, including the timestamp, so the receiver can reproduce it.
3. Verify the signature on receipt
The receiver recomputes the HMAC over the raw body and compares with a constant-time check:
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
return res.status(401).end();
}
Use the raw body, not the parsed JSON, since re-serializing changes the bytes.
4. Make delivery idempotent
Networks cause duplicates. The receiver records each event id and ignores ones it has already processed:
if (await redis.set(`evt:${id}`, '1', 'NX', 'EX', 86400) === null) {
return res.status(200).end(); // already handled
}
5. Retry with backoff
The sender retries failed deliveries (non-2xx, timeout) with exponential backoff and jitter, up to a cap, then moves the event to a dead-letter queue for inspection.
6. Prevent replay attacks
Reject deliveries whose signed timestamp is older than a few minutes, so a captured request cannot be replayed later.
Verification
- A correctly signed payload is accepted; a tampered one returns 401.
- A duplicate event id is processed only once.
- A failed delivery is retried with increasing delays.
- A stale-timestamp request is rejected.
Next Steps
Let consumers register endpoints and rotate their own secrets, expose delivery logs and a manual replay button, and document your event types and signature scheme so integrators can verify reliably.
Prerequisites
- A service that emits events
- A receiver endpoint over HTTPS
- Basic crypto knowledge
Steps
- 1Design the event payload
- 2Sign payloads with HMAC
- 3Verify the signature on receipt
- 4Make delivery idempotent
- 5Retry with backoff
- 6Prevent replay attacks