Webhooks Explained: Signed Payloads and Retry Logic
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
- Create the payload (JSON).
- Compute HMAC-SHA256 of the payload using the secret.
- Include the hex digest in a header, e.g.,
X-Signature: sha256=.... - 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
- Exponential backoff: Wait 1s, 2s, 4s, 8s, etc.
- Jitter: Add randomness to avoid thundering herd.
- Idempotency: Use an idempotency key so duplicate deliveries don't cause duplicate actions.
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
- Always use HTTPS for webhook endpoints.
- Rotate secrets periodically.
- Validate the payload schema before processing.
- Respond quickly (within 2xx) and process asynchronously.
- Monitor webhook delivery metrics (success rate, latency).
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.