Signature Verification
Verify webhook authenticity using HMAC-SHA256 signatures
Every webhook request includes an X-Signature header containing an HMAC-SHA256 signature of the request body. Verify this signature to ensure the webhook was sent by Octopus and has not been tampered with.
How Signing Works
- Octopus computes
HMAC-SHA256(request_body, your_secret) - The result is hex-encoded and sent in the
X-Signatureheader - You compute the same HMAC on the received body using your secret
- If the signatures match, the webhook is authentic
Verification Headers
| Header | Purpose |
|---|---|
X-Signature | HMAC-SHA256 hex digest of the raw request body |
X-OCTOPUS-WEBHOOK-TOKEN | Your shared secret (for quick lookup) |
X-Timestamp | Unix timestamp — use to reject stale webhooks |
X-Event-ID | Unique event ID — use for idempotency |
Implementation Examples
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
// Read the raw body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Bad request", 400)
return
}
// Get the signature from the header
receivedSig := r.Header.Get("X-Signature")
secret := "your_webhook_secret"
// Compute expected signature
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expectedSig := hex.EncodeToString(mac.Sum(nil))
// Constant-time comparison to prevent timing attacks
if !hmac.Equal([]byte(receivedSig), []byte(expectedSig)) {
http.Error(w, "Invalid signature", 401)
return
}
// Signature valid — process the event
w.WriteHeader(http.StatusOK)
}const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const receivedSig = req.headers['x-signature'];
const secret = 'your_webhook_secret';
// Compute expected signature
const expectedSig = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
// Constant-time comparison
if (!crypto.timingSafeEqual(
Buffer.from(receivedSig, 'hex'),
Buffer.from(expectedSig, 'hex')
)) {
return res.status(401).send('Invalid signature');
}
// Signature valid — process the event
const event = JSON.parse(req.body);
console.log(`Received ${event.type}: ${event.id}`);
res.sendStatus(200);
});import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks', methods=['POST'])
def webhook():
received_sig = request.headers.get('X-Signature')
secret = 'your_webhook_secret'
body = request.get_data()
# Compute expected signature
expected_sig = hmac.new(
secret.encode(),
body,
hashlib.sha256
).hexdigest()
# Constant-time comparison
if not hmac.compare_digest(received_sig, expected_sig):
abort(401)
# Signature valid — process the event
event = request.get_json()
print(f"Received {event['type']}: {event['id']}")
return '', 200Timestamp Validation
Use the X-Timestamp header to reject stale or replayed webhooks. A common approach is to reject webhooks older than 5 minutes:
timestamp := r.Header.Get("X-Timestamp")
ts, _ := strconv.ParseInt(timestamp, 10, 64)
age := time.Since(time.Unix(ts, 0))
if age > 5*time.Minute {
http.Error(w, "Webhook too old", 401)
return
}