Webhooks Explained: Signed Payloads and Retry Strategies
You've built an API that sends webhooks to your users' endpoints. But then you face the classic problems: how do you ensure the payload hasn't been tampered with? What happens when the endpoint is down? How do you prevent duplicate processing? In this article, we'll tackle these challenges head-on with signed payloads and robust retry strategies.
Why Webhooks Need Signatures
Webhooks are HTTP callbacks that notify other systems about events. Because they're sent over the public internet, they're vulnerable to interception and spoofing. Without a signature, a malicious actor could send fake webhooks to your users, triggering unintended actions. Signing payloads with a shared secret ensures authenticity and integrity.
How Signed Payloads Work
The most common approach is HMAC (Hash-based Message Authentication Code) with SHA-256. The sender and receiver share a secret key. The sender computes a signature over the payload (and sometimes a timestamp) and includes it in a header. The receiver recomputes the signature and compares it to the received one.
Step-by-Step: Signing a Payload
- Create the payload: Serialize your event data to JSON. Ensure consistent formatting (e.g., no extra whitespace) because the signature is computed over the exact bytes.
- Add a timestamp: Include a Unix timestamp in the payload or header to prevent replay attacks. The receiver can reject requests older than a few minutes.
- Compute the HMAC: Use your secret key to generate the HMAC-SHA256 of the payload string (or timestamp + payload).
- Attach the signature: Send it in a header like
X-Signature.
Here's an example in Python:
import hmac
import hashlib
import json
import time
secret = b'your_shared_secret'
payload = {"event": "user.created", "data": {"id": 123}}
body = json.dumps(payload, separators=(',', ':'))
timestamp = str(int(time.time()))
signed_content = timestamp + '.' + body
signature = hmac.new(secret, signed_content.encode(), hashlib.sha256).hexdigest()
# Send headers: X-Signature: signature, X-Timestamp: timestamp
Verifying the Signature
On the receiving end, you must:
- Extract the timestamp and signature from headers.
- Reject if the timestamp is too old (e.g., >5 minutes) to prevent replay attacks.
- Recompute the signature using the same method.
- Compare using a constant-time function to avoid timing attacks.
Example verification in Node.js:
const crypto = require('crypto');
function verifySignature(req, secret) {
const signature = req.headers['x-signature'];
const timestamp = req.headers['x-timestamp'];
if (!signature || !timestamp) return false;
if (Date.now() / 1000 - timestamp > 300) return false; // 5 min
const body = JSON.stringify(req.body);
const signed = timestamp + '.' + body;
const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Designing Retry Strategies
Even with signatures, webhooks can fail due to network issues, server errors, or timeouts. A robust retry strategy ensures delivery without overwhelming the receiver.
Key Considerations
- Idempotency: Receivers should handle duplicate events gracefully. Use an idempotency key (e.g., event ID) to deduplicate.
- Exponential backoff: Retry after increasing delays (e.g., 1s, 5s, 30s, 5m, 30m) to give the receiver time to recover.
- Maximum retries: Set a limit (e.g., 5 attempts) and then mark the webhook as failed, perhaps alerting the user.
- Timeouts: Set a reasonable timeout (e.g., 10 seconds) for each attempt.
Implementing Retries
Most webhook senders use a queue system (e.g., Redis, RabbitMQ, or a database) to schedule retries. Here's a simplified flow:
- On event trigger, enqueue a webhook job with the payload and target URL.
- Worker picks up the job and attempts delivery.
- If the response status is 2xx, mark as success.
- If not, schedule a retry with exponential backoff, up to max attempts.
- After max attempts, mark as failed and notify.
Example retry schedule:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
Best Practices for Webhook Security
- Use HTTPS: Always deliver webhooks over TLS to prevent eavesdropping.
- Rotate secrets: Periodically rotate shared secrets and support multiple active secrets during rotation.
- Validate URLs: Ensure the webhook URL is HTTPS and not pointing to internal IPs (to prevent SSRF).
- Log and monitor: Keep logs of delivery attempts and failures for debugging and auditing.
FAQ
What is a signed webhook payload?
A signed payload includes a cryptographic signature (usually HMAC) that allows the receiver to verify the sender's authenticity and that the payload hasn't been tampered with.
How many times should I retry a failed webhook?
There's no one-size-fits-all answer, but a common approach is 3–5 retries with exponential backoff. Consider the criticality of the event and the receiver's capacity.
Can I use JWT for webhook signatures?
Yes, JWT can be used, but it's more complex. HMAC is simpler and sufficient for most use cases. JWT is useful if you need to include claims or expiration within the signature itself.
Ready to test your webhook payloads? Use our JSON Formatter to validate and pretty-print your JSON payloads before sending.