Verify Webhook Signatures
Verify HMAC-SHA256 signatures to ensure webhook payloads are authentic.
Signature Verification
Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 signature. Always verify this signature before processing the payload.
How Signing Works
- ThinnestAI constructs:
{timestamp}.{raw_json_body} - Signs with HMAC-SHA256 using your endpoint's secret
- Sends as
X-Webhook-Signature: sha256={hex_digest}
Python
import hmac
import hashlib
import time
def verify_webhook(request_body: bytes, headers: dict, secret: str) -> bool:
signature = headers.get("X-Webhook-Signature", "")
timestamp = headers.get("X-Webhook-Timestamp", "")
# Reject old timestamps (replay protection — 5 min window)
if abs(time.time() - int(timestamp)) > 300:
return False
# Reconstruct expected signature
message = f"{timestamp}.{request_body.decode()}".encode()
expected = "sha256=" + hmac.new(
secret.encode(), message, hashlib.sha256
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(signature, expected)FastAPI Example
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
WEBHOOK_SECRET = "your_endpoint_secret_here"
@app.post("/webhooks/thinnestai")
async def handle_webhook(request: Request):
body = await request.body()
headers = dict(request.headers)
if not verify_webhook(body, headers, WEBHOOK_SECRET):
raise HTTPException(401, "Invalid signature")
import json
payload = json.loads(body)
event = payload["event"]
data = payload["data"]
if event == "chat.message.completed":
print(f"Agent responded: {data.get('message', '')[:100]}")
elif event == "voice.call.ended":
print(f"Call ended, duration: {data.get('duration', 0)}s")
return {"ok": True}Node.js
const crypto = require('crypto');
function verifyWebhook(rawBody, headers, secret) {
const signature = headers['x-webhook-signature'] || '';
const timestamp = headers['x-webhook-timestamp'] || '';
// Replay protection (5 min window)
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return false;
}
const message = `${timestamp}.${rawBody}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Express Example
const express = require('express');
const app = express();
const WEBHOOK_SECRET = 'your_endpoint_secret_here';
app.post('/webhooks/thinnestai', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyWebhook(req.body.toString(), req.headers, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(req.body);
console.log(`Event: ${payload.event}`, payload.data);
res.json({ ok: true });
});Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"math"
"strconv"
"time"
)
func verifyWebhook(body []byte, signature, timestamp, secret string) bool {
// Replay protection
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil || math.Abs(float64(time.Now().Unix()-ts)) > 300 {
return false
}
message := fmt.Sprintf("%s.%s", timestamp, string(body))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}Best Practices
- Always verify signatures before processing payloads
- Check timestamps to prevent replay attacks (5 minute window)
- Use constant-time comparison (
hmac.compare_digestin Python,crypto.timingSafeEqualin Node.js) - Respond with 2xx quickly — do heavy processing asynchronously
- Handle duplicates — use
X-Webhook-Delivery-Idfor idempotency - Store the secret securely — treat it like an API key