Webhook Best Practices
Production patterns for both sides of a webhook integration: the producer (sender) and the consumer (receiver).
For Producers (Sending Webhooks)
Sign Every Payload
Every outgoing webhook should include an HMAC-SHA256 signature in the headers. This lets consumers verify the payload hasn't been tampered with. HookSniff does this automatically — every delivery is signed with the endpoint's signing secret.
webhook-signature: v1,base64(hmac_sha256(secret, "{id}.{timestamp}.{body}"))
webhook-timestamp: 1705312200
webhook-id: msg_abc123Never sign a re-serialized version of the payload. Byte differences will break verification. Use the raw request body.
Include an Idempotency Key
Add a unique identifier to each event so consumers can deduplicate:
curl -X POST https://hooksniff-api-1046140057667.europe-west1.run.app/v1/webhooks \
-H "Authorization: Bearer hr_live_YOUR_KEY" \
-H "Idempotency-Key: order-12345-created" \
-H "Content-Type: application/json" \
-d '{
"endpoint_id": "ep_abc123",
"event": "order.created",
"data": {
"order_id": "12345",
"total": 99.99,
"currency": "USD"
}
}'Use business logic identifiers (e.g., order-{id}-created), not random UUIDs. This ensures the same operation always produces the same key.
Design Payloads for Stability
Keep payloads self-contained. Include all the data the consumer needs rather than forcing them to make API calls back to you.
Good — Self-contained
{
"event": "order.shipped",
"data": {
"order_id": "ord_123",
"tracking_number": "1Z999AA10123456784",
"carrier": "ups",
"shipped_at": "2026-01-15T14:00:00Z"
}
}Bad — Requires follow-up call
{
"event": "order.shipped",
"data": {
"order_id": "ord_123"
}
}Use Consistent Event Type Naming
Use resource.action format with dot separators:
order.created,order.updated,order.cancelledpayment.succeeded,payment.faileduser.created,user.updated
Be specific: invoice.payment_failed is better than invoice.error.
For Consumers (Receiving Webhooks)
Verify Signatures First
Before processing any webhook, verify the HMAC signature. Reject requests with missing or invalid signatures immediately.
from hooksniff import Webhook
wh = Webhook("whsec_your_secret")
@app.route("/webhook", methods=["POST"])
def handle_webhook():
try:
payload = wh.verify(
request.data,
{
"webhook-id": request.headers["webhook-id"],
"webhook-timestamp": request.headers["webhook-timestamp"],
"webhook-signature": request.headers["webhook-signature"],
},
)
# ✅ Valid — process event
return "", 200
except Exception:
return "Invalid signature", 401Use constant-time comparison (hmac.compare_digest) to prevent timing attacks.
Respond Fast, Process Later
Return a 200 OK within 5 seconds. Do the actual processing asynchronously:
import { Webhook } from 'hooksniff';
const wh = new Webhook('whsec_your_secret');
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
// 1. Verify signature (fast)
try {
const payload = wh.verify(req.body, {
'webhook-id': req.headers['webhook-id'],
'webhook-timestamp': req.headers['webhook-timestamp'],
'webhook-signature': req.headers['webhook-signature'],
});
} catch (err) {
return res.status(401).send('Invalid signature');
}
// 2. Respond immediately
res.status(200).json({ received: true });
// 3. Process asynchronously
processWebhookAsync(JSON.parse(req.body));
});If your endpoint takes too long, HookSniff may time out and retry, leading to duplicate processing.
Handle Duplicates
Webhooks can be delivered more than once. Track processed delivery IDs:
CREATE TABLE processed_webhooks (
delivery_id TEXT PRIMARY KEY,
processed_at TIMESTAMPTZ DEFAULT NOW()
);
-- Before processing, check if already processed
SELECT 1 FROM processed_webhooks WHERE delivery_id = $1;Use HTTPS Only
Always expose webhook endpoints over HTTPS. HookSniff only delivers to HTTPS endpoints by default. HTTP endpoints are rejected unless explicitly allowed.
Monitoring and Alerting
Track these metrics to catch issues early:
| Metrik | Target | Alert When |
|---|---|---|
| Delivery success rate | >99.5% | <99% |
| P95 delivery latency | <2s | >5s |
| Consecutive failures per endpoint | 0 | >5 |
| DLQ depth | 0 | >100 |
Use HookSniff's built-in alerts to get notified when these thresholds are crossed.
Payload Design Guidelines
Use a Consistent Envelope
Every event should follow the same top-level structure:
{
"event": "order.created",
"data": {
"order_id": "12345",
"total": 99.99,
"currency": "USD",
"items": [...]
},
"timestamp": "2026-01-15T10:30:00Z"
}Timestamp Format
Always use ISO 8601 with timezone: 2026-01-15T10:30:00Z. Never use Unix timestamps in payloads — they are harder to read when debugging.
Version Your Events
When you change payload shapes, include a version in the event type or payload. Support old versions for at least 6 months after announcing deprecation. Example: order.created.v2.