Skip to main content

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.

Difficulty
Intermediate
Duration
45 minutes
Steps
6

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

  • 1
    Design the event payload
  • 2
    Sign payloads with HMAC
  • 3
    Verify the signature on receipt
  • 4
    Make delivery idempotent
  • 5
    Retry with backoff
  • 6
    Prevent replay attacks

Category

API Design