Webhooks
Webhook Security
Verify HMAC-SHA256 webhook signatures to ensure requests come from HMS Sovereign.
When you configure a webhook_secret on your assistant, HMS Sovereign signs all webhook requests. You should verify these signatures to ensure requests come from HMS Sovereign.
Signature Format
HMS Sovereign uses HMAC-SHA256 to sign webhooks. The signature is included in the X-Webhook-Signature header.
How It's Calculated
message = timestamp + "." + raw_request_body
signature = "sha256=" + HMAC-SHA256(secret, message)The X-Webhook-Signature header value is prefixed with sha256=. Strip this prefix before comparing with your computed HMAC.
Verification Examples
Python
import hmac
import hashlib
def verify_webhook_signature(payload: str, secret: str, timestamp: str, signature: str) -> bool:
"""
Verify HMS Sovereign webhook signature.
Args:
payload: Raw request body as string
secret: Your webhook_secret from assistant config
timestamp: X-Webhook-Timestamp header value
signature: X-Webhook-Signature header value (e.g. "sha256=abc123...")
Returns:
True if signature is valid
"""
# Strip the "sha256=" prefix from the header value
sig_hex = signature.removeprefix("sha256=")
message = f"{timestamp}.{payload}"
expected = hmac.new(
secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(sig_hex, expected)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "your-secret-here"
@app.route('/webhooks/hms-sovereign', methods=['POST'])
def handle_webhook():
payload = request.get_data(as_text=True)
timestamp = request.headers.get('X-Webhook-Timestamp')
signature = request.headers.get('X-Webhook-Signature')
if not verify_webhook_signature(payload, WEBHOOK_SECRET, timestamp, signature):
abort(401, 'Invalid signature')
# Process webhook...
data = request.json
# ...
return 'OK', 200Node.js
const crypto = require('crypto');
function verifyWebhookSignature(payload, secret, timestamp, signature) {
// Strip the "sha256=" prefix from the header value
const sigHex = signature.replace(/^sha256=/, '');
const message = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(sigHex),
Buffer.from(expected)
);
}
// Express example
const express = require('express');
const app = express();
const WEBHOOK_SECRET = 'your-secret-here';
app.post('/webhooks/hms-sovereign',
express.raw({ type: 'application/json' }),
(req, res) => {
const payload = req.body.toString();
const timestamp = req.headers['x-webhook-timestamp'];
const signature = req.headers['x-webhook-signature'];
if (!verifyWebhookSignature(payload, WEBHOOK_SECRET, timestamp, signature)) {
return res.status(401).send('Invalid signature');
}
const data = JSON.parse(payload);
// Process webhook...
res.status(200).send('OK');
}
);Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
)
func verifyWebhookSignature(payload, secret, timestamp, signature string) bool {
// Strip the "sha256=" prefix from the header value
sigHex := strings.TrimPrefix(signature, "sha256=")
message := timestamp + "." + payload
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(sigHex), []byte(expected))
}PHP
<?php
function verifyWebhookSignature(
string $payload,
string $secret,
string $timestamp,
string $signature
): bool {
// Strip the "sha256=" prefix from the header value
$sigHex = str_replace('sha256=', '', $signature);
$message = $timestamp . '.' . $payload;
$expected = hash_hmac('sha256', $message, $secret);
return hash_equals($expected, $sigHex);
}
// Usage
$payload = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'];
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];
if (!verifyWebhookSignature($payload, WEBHOOK_SECRET, $timestamp, $signature)) {
http_response_code(401);
exit('Invalid signature');
}Best Practices
- Always verify in production - Never skip signature verification in production
- Use timing-safe comparison - Prevent timing attacks with constant-time comparison
- Check timestamp freshness - Optionally reject requests older than 5 minutes to prevent replay attacks
- Store secret securely - Use environment variables, not hardcoded values
- Log verification failures - Monitor for suspicious activity
Timestamp Validation
Optionally validate the timestamp to prevent replay attacks:
import time
def verify_webhook(payload, secret, timestamp, signature, max_age_seconds=300):
# Verify signature
if not verify_webhook_signature(payload, secret, timestamp, signature):
return False
# Check timestamp freshness
current_time = int(time.time())
request_time = int(timestamp)
if abs(current_time - request_time) > max_age_seconds:
return False
return TrueSee Webhook Overview for header details.