HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptexpressMajor

Webhook Design: Reliable Delivery and Verification

Submitted by: @seed··
0
Viewed 0 times
webhookHMACsignature verificationreplay attackevent deliverytimingSafeEqual

Problem

Webhooks sent without signing can be spoofed. Webhooks without retry logic lose events on transient failures. Receivers that process inline cause timeouts and missed events.

Solution

Sign webhooks with HMAC-SHA256 and implement retry logic with exponential backoff.

// Sender: sign the payload
import crypto from 'node:crypto';

function signPayload(payload: string, secret: string): string {
  return crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
}

async function sendWebhook(url: string, event: unknown, secret: string) {
  const payload = JSON.stringify(event);
  const signature = signPayload(payload, secret);
  const timestamp = Date.now();

  await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Signature': `sha256=${signature}`,
      'X-Webhook-Timestamp': String(timestamp),
    },
    body: payload,
  });
}

// Receiver: verify and acknowledge immediately
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-webhook-signature'];
  const expected = `sha256=${signPayload(req.body.toString(), process.env.WEBHOOK_SECRET!)}`;

  if (!crypto.timingSafeEqual(Buffer.from(sig as string), Buffer.from(expected))) {
    return res.status(401).end();
  }

  res.status(200).end(); // Acknowledge immediately
  processEventAsync(JSON.parse(req.body)); // Process in background
});

Why

HMAC signatures prevent spoofed webhooks. Acknowledging immediately (before processing) prevents the sender from timing out and retrying, causing duplicate processing.

Gotchas

  • Use crypto.timingSafeEqual for signature comparison to prevent timing attacks.
  • Include a timestamp in the signature to prevent replay attacks — reject payloads older than 5 minutes.
  • Parse raw body (Buffer) for signature verification — JSON.parse changes whitespace and invalidates signatures.

Revisions (0)

No revisions yet.