Webhooks Explained: Signed Payloads and Retries
You've just integrated a payment provider's webhook to receive transaction updates. Everything works in testing, but in production you notice missing events during traffic spikes or duplicate charges when the provider retries. Webhooks are simple in concept but tricky in practice: they cross network boundaries, can be delayed, duplicated, or spoofed. This article explains how to build robust webhook consumers and producers using signed payloads and smart retry strategies.
What Are Webhooks?
Webhooks are user-defined HTTP callbacks triggered by events. When something happens in a source system (e.g., a new order), it sends an HTTP POST request to a URL you provide. Unlike polling, webhooks push data in near real-time, reducing latency and server load.
However, webhooks introduce challenges: ensuring the request actually came from the expected sender, handling failures gracefully, and avoiding duplicate processing.
Securing Webhooks with Signed Payloads
Without verification, anyone can send fake requests to your webhook endpoint. Signed payloads solve this by including a cryptographic signature that you can validate.
How HMAC Signatures Work
The sender computes a hash-based message authentication code (HMAC) using a shared secret and the request body. The signature is sent in a header (e.g., X-Signature). You recompute the HMAC on your side and compare.
// Node.js example: verifying a webhook signature
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Always use a constant-time comparison function like timingSafeEqual to prevent timing attacks.
Include a Timestamp
To prevent replay attacks, include a timestamp in the signed payload and reject requests older than a few minutes (e.g., 5 minutes). The timestamp should be part of the signed data.
Implementing Reliable Retries
Network issues or temporary server errors can cause webhook delivery to fail. A robust sender retries failed deliveries with exponential backoff.
Retry Strategy
- Exponential backoff: Wait longer between each retry (e.g., 1s, 2s, 4s, 8s...).
- Maximum retries: Set a limit (e.g., 5 attempts) to avoid infinite loops.
- Dead-letter queue: After max retries, store the event for manual inspection.
- Idempotency: Include a unique event ID so the receiver can deduplicate.
Example: Retry with Backoff in Python
import time
import requests
from requests.exceptions import RequestException
def send_webhook(url, payload, max_retries=5):
for attempt in range(max_retries):
try:
response = requests.post(url, json=payload, timeout=5)
if response.status_code < 500:
return True # success or client error (no retry)
except RequestException:
pass
time.sleep(2 ** attempt) # exponential backoff
return False
Best Practices for Webhook Consumers
- Respond quickly: Acknowledge receipt with a 2xx status before processing. Queue the work asynchronously.
- Validate signatures: Always verify the signature before trusting the payload.
- Handle duplicates: Use the event ID to ensure idempotent processing.
- Log everything: Keep logs of received webhooks for debugging.
Comparison: Webhooks vs Polling
| Aspect | Webhooks | Polling |
|---|---|---|
| Latency | Near real-time | Depends on interval |
| Server load | Low (push only) | High (frequent requests) |
| Complexity | Higher (need endpoint, security) | Lower |
| Reliability | Requires retries | Inherently retried |
FAQ
What is a signed webhook payload?
A signed payload includes a cryptographic signature (usually HMAC) that allows the receiver to verify the request's authenticity and integrity.
How many times should I retry a failed webhook?
A common practice is 3–5 retries with exponential backoff, but it depends on your SLA and the criticality of the event.
Can I use webhooks without HTTPS?
No, always use HTTPS to protect the payload and signature from interception. Plain HTTP exposes sensitive data.
When debugging webhook payloads, you might need to format JSON quickly. Try our JSON Formatter to pretty-print and validate JSON responses.