> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thinnest.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 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

1. ThinnestAI constructs: `{timestamp}.{raw_json_body}`
2. Signs with HMAC-SHA256 using your endpoint's secret
3. Sends as `X-Webhook-Signature: sha256={hex_digest}`

### Python

```python theme={null}
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

```python theme={null}
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

```javascript theme={null}
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

```javascript theme={null}
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

```go theme={null}
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_digest` in Python, `crypto.timingSafeEqual` in Node.js)
* **Respond with 2xx quickly** — do heavy processing asynchronously
* **Handle duplicates** — use `X-Webhook-Delivery-Id` for idempotency
* **Store the secret securely** — treat it like an API key
