Webhooks Explained: Signed Payloads and Retry Logic

Backend2026-10-28TryQuickToolBox

You've built an API and now need to notify other services when events happen. Polling is inefficient, so you turn to webhooks. But soon you discover that webhooks can fail, be spoofed, or cause duplicate processing. This article explains how to secure webhooks with signed payloads and implement robust retry logic.

What Are Webhooks?

Webhooks are user-defined HTTP callbacks. When an event occurs in your system (e.g., a payment succeeds), you send an HTTP POST request to a URL provided by the subscriber. The payload typically contains event data in JSON or form format.

Webhooks enable real-time communication between services without constant polling. They are widely used by payment gateways, CI/CD tools, and SaaS platforms.

Why Secure Webhooks?

Without security, anyone who knows the webhook URL can send fake requests, leading to data corruption or fraud. Also, attackers might replay old requests. Signing payloads with a shared secret ensures authenticity and integrity.

Signing Payloads with HMAC

HMAC (Hash-based Message Authentication Code) is a common method to sign webhooks. The sender and receiver share a secret key. The sender computes an HMAC of the payload using the secret and includes it in a header (e.g., X-Signature). The receiver recomputes the HMAC and compares it.

Step-by-Step: Sending a Signed Webhook

  1. Create the payload (JSON).
  2. Compute HMAC-SHA256 of the payload using the secret.
  3. Include the hex digest in a header, e.g., X-Signature: sha256=....
  4. Send the POST request.
// Node.js example using crypto
const crypto = require('crypto');
const secret = 'your_shared_secret';
const payload = JSON.stringify({ event: 'payment.success', amount: 100 });
const signature = crypto.createHmac('sha256', secret).update(payload).digest('hex');
// Send POST with header X-Signature: sha256=signature

Verifying the Signature

On the receiver side, extract the signature header, compute the HMAC of the raw request body, and compare using a constant-time comparison to prevent timing attacks.

// Express.js example
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-signature'];
  const computed = crypto.createHmac('sha256', secret).update(req.rawBody).digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(computed))) {
    return res.status(401).send('Invalid signature');
  }
  // Process payload
  res.status(200).send('OK');
});

Note: Ensure you use the raw body, not parsed JSON, as parsing can alter the payload.

Preventing Replay Attacks

Include a timestamp in the signed payload and reject requests older than a few minutes. Also, include a unique event ID and store processed IDs to avoid duplicates.

Retry Logic for Reliable Delivery

Webhook endpoints can be down or slow. Implement retries with exponential backoff and jitter. Set a maximum number of retries (e.g., 5) and a timeout for each request.

Retry Strategy

Example retry schedule: 1st retry after 5s, 2nd after 30s, 3rd after 2min, 4th after 10min, 5th after 1h.

Handling Failures

If all retries fail, log the failure and notify the subscriber (e.g., via email or dashboard). Provide a way to manually resend webhooks.

Comparison: Signed Payloads vs. Basic Auth

Method Security Complexity Replay Protection
HMAC Signed Payload High Moderate Yes (with timestamp)
Basic Auth Low Low No
API Key in Header Medium Low No

Best Practices

FAQ

How do I verify a webhook signature?

Compute the HMAC of the raw request body using your secret and compare it to the signature header using a constant-time comparison.

What should I do if a webhook fails?

Implement retries with exponential backoff and jitter. If all retries fail, log the failure and provide a manual resend option.

Can I use webhooks without signing?

It's not recommended. Without signing, you can't verify the sender, making your system vulnerable to spoofing and replay attacks.

When testing webhook payloads, you might need to inspect JSON responses. Use our JSON Formatter to pretty-print and validate JSON data quickly.