Webhooks
Best practices for receiving, verifying, and processing webhooks reliably
Verify Every Webhook
Never trust a webhook based solely on the X-OCTOPUS-WEBHOOK-TOKEN header. Always compute the HMAC-SHA256 signature of the raw request body using your secret and compare it against the X-Signature header. This proves both authenticity (it came from Octopus) and integrity (the body hasn't been tampered with).
Use constant-time comparison (e.g. hmac.Equal in Go, crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python) to prevent timing-based side-channel attacks.
See the Signature Verification page for full implementation examples.
Validate Timestamps
Check the X-Timestamp header and reject webhooks older than 5 minutes. This prevents replay attacks where an attacker resends a captured webhook request.
ts, _ := strconv.ParseInt(r.Header.Get("X-Timestamp"), 10, 64)
if time.Since(time.Unix(ts, 0)) > 5*time.Minute {
http.Error(w, "Webhook too old", 401)
return
}Process Idempotently
The same event may be delivered more than once due to retries. Always deduplicate by X-Event-ID:
- Before processing, check if you've already handled this event ID
- If yes, return
200 OKwithout re-processing - If no, process the event and record the ID
Use a database table or cache (e.g. Redis SET NX with a TTL) to track processed event IDs.
Respond Quickly
Your endpoint must return a 2xx response within 30 seconds. If it times out or returns a non-2xx status, the delivery is retried.
Do not perform heavy processing inline. Instead:
- Receive the webhook and validate the signature
- Persist the raw payload to a queue or database
- Return
200 OKimmediately - Process the event asynchronously in a background worker
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
// 1. Verify signature (fail fast)
if !verifySignature(body, r.Header.Get("X-Signature"), secret) {
http.Error(w, "Invalid signature", 401)
return
}
// 2. Queue for async processing
queue.Enqueue("webhook_events", body)
// 3. Respond immediately
w.WriteHeader(http.StatusOK)
}Secure Your Secret
- Store your webhook secret in environment variables or a secrets manager — never in source code
- Rotate your secret periodically by updating it in both your dashboard and your application
- If you suspect your secret has been compromised, rotate it immediately
Handle Retries Gracefully
Octopus retries failed deliveries up to 5 times with exponential backoff (1m, 2m, 4m, 8m, 16m). Design your endpoint to handle retries:
- Return
200 OKfor duplicate events (idempotent processing) - Return
200 OKeven if you don't recognise the event type (forward compatibility) - Only return non-2xx if you genuinely cannot accept the webhook (e.g. signature invalid)
Returning a 4xx or 5xx response triggers a retry. If your handler has a bug that always returns an error, you will receive the same webhook 6 times. Fix errors promptly and return 200 for events you don't need to process.
Use HTTPS
Webhook URLs must use HTTPS. This ensures the payload (which may contain order details) is encrypted in transit. Self-signed certificates are not supported.
Monitor Delivery
Check your dashboard for failed webhook deliveries. Common issues:
- Timeouts — your endpoint is too slow. Offload processing to a background queue.
- SSL errors — your certificate has expired or is misconfigured.
- Connection refused — your server is down or firewall is blocking Octopus IPs.
- Non-2xx responses — your handler is returning an error. Check your application logs.
Checklist
- Verify
X-Signatureusing HMAC-SHA256 with constant-time comparison - Validate
X-Timestampto reject stale webhooks (> 5 minutes) - Deduplicate by
X-Event-IDfor idempotent processing - Respond within 30 seconds with
200 OK - Process heavy logic asynchronously (queue + worker)
- Store secret in environment variables, not source code
- Return
200 OKfor unrecognised event types - Monitor delivery failures in your dashboard