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

# Security

> Verify webhook signatures to ensure requests are from Zavu

Zavu signs all webhook requests with an HMAC-SHA256 signature. You should verify this signature to ensure the request is authentic and hasn't been tampered with.

## Signature Header

Every webhook request includes an `X-Zavu-Signature` header:

```
X-Zavu-Signature: t=1705312200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

The header contains:

| Part | Description                                               |
| ---- | --------------------------------------------------------- |
| `t`  | Unix timestamp (seconds) when the signature was generated |
| `v1` | HMAC-SHA256 signature of the payload                      |

## Verifying Signatures

### Step 1: Extract the Signature

Parse the `X-Zavu-Signature` header to get the timestamp and signature:

```javascript theme={null}
function parseSignature(header) {
  const parts = header.split(',');
  const timestamp = parts.find(p => p.startsWith('t=')).slice(2);
  const signature = parts.find(p => p.startsWith('v1=')).slice(3);
  return { timestamp: parseInt(timestamp), signature };
}
```

### Step 2: Prepare the Signed Payload

Concatenate the timestamp and the raw request body:

```javascript theme={null}
const signedPayload = `${timestamp}.${rawBody}`;
```

### Step 3: Compute the Expected Signature

Use HMAC-SHA256 with your webhook secret:

```javascript theme={null}
const crypto = require('crypto');

function computeSignature(secret, payload) {
  return crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
}
```

### Step 4: Compare Signatures

Use a constant-time comparison to prevent timing attacks:

```javascript theme={null}
const crypto = require('crypto');

function verifySignature(expected, actual) {
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(actual)
  );
}
```

## Complete Examples

<CodeGroup>
  ```typescript TypeScript (Express) theme={null}
  import crypto from "crypto";
  import express from "express";

  const app = express();

  // Use raw body for signature verification
  app.use("/webhooks/zavu", express.raw({ type: "application/json" }));

  function verifyZavuSignature(req, secret) {
    const signatureHeader = req.headers["x-zavu-signature"];
    if (!signatureHeader) {
      return false;
    }

    // Parse the signature header
    const parts = signatureHeader.split(",");
    const timestampPart = parts.find(p => p.startsWith("t="));
    const signaturePart = parts.find(p => p.startsWith("v1="));

    if (!timestampPart || !signaturePart) {
      return false;
    }

    const timestamp = parseInt(timestampPart.slice(2));
    const signature = signaturePart.slice(3);

    // Check timestamp (reject if older than 5 minutes)
    const now = Math.floor(Date.now() / 1000);
    if (now - timestamp > 300) {
      console.log("Webhook timestamp too old");
      return false;
    }

    // Compute expected signature
    const rawBody = req.body.toString();
    const signedPayload = `${timestamp}.${rawBody}`;
    const expectedSignature = crypto
      .createHmac("sha256", secret)
      .update(signedPayload)
      .digest("hex");

    // Constant-time comparison
    try {
      return crypto.timingSafeEqual(
        Buffer.from(expectedSignature),
        Buffer.from(signature)
      );
    } catch {
      return false;
    }
  }

  app.post("/webhooks/zavu", (req, res) => {
    const isValid = verifyZavuSignature(req, process.env.ZAVU_WEBHOOK_SECRET);

    if (!isValid) {
      console.log("Invalid webhook signature");
      return res.status(401).send("Invalid signature");
    }

    // Parse the body and process
    const event = JSON.parse(req.body.toString());
    console.log("Verified webhook:", event.type);

    res.status(200).send("OK");
  });

  app.listen(3000);
  ```

  ```python Python (Flask) theme={null}
  import hmac
  import hashlib
  import time
  from flask import Flask, request

  app = Flask(__name__)

  WEBHOOK_SECRET = 'whsec_your_secret_here'  # From sender.webhook.secret

  def verify_zavu_signature(request, secret):
      signature_header = request.headers.get('X-Zavu-Signature')
      if not signature_header:
          return False

      # Parse the signature header
      parts = signature_header.split(',')
      timestamp_part = next((p for p in parts if p.startswith('t=')), None)
      signature_part = next((p for p in parts if p.startswith('v1=')), None)

      if not timestamp_part or not signature_part:
          return False

      timestamp = int(timestamp_part[2:])
      signature = signature_part[3:]

      # Check timestamp (reject if older than 5 minutes)
      now = int(time.time())
      if now - timestamp > 300:
          print('Webhook timestamp too old')
          return False

      # Compute expected signature
      raw_body = request.data.decode('utf-8')
      signed_payload = f'{timestamp}.{raw_body}'
      expected_signature = hmac.new(
          secret.encode('utf-8'),
          signed_payload.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()

      # Constant-time comparison
      return hmac.compare_digest(expected_signature, signature)

  @app.route('/webhooks/zavu', methods=['POST'])
  def handle_webhook():
      if not verify_zavu_signature(request, WEBHOOK_SECRET):
          print('Invalid webhook signature')
          return 'Invalid signature', 401

      event = request.json
      print(f'Verified webhook: {event["type"]}')

      return 'OK', 200

  if __name__ == '__main__':
      app.run(port=3000)
  ```

  ```ruby Ruby (Sinatra) theme={null}
  require "sinatra"
  require "openssl"
  require "json"

  WEBHOOK_SECRET = ENV["ZAVU_WEBHOOK_SECRET"] # From sender.webhook.secret

  def verify_zavu_signature(request, secret)
    signature_header = request.env["HTTP_X_ZAVU_SIGNATURE"]
    return false unless signature_header

    # Parse the signature header
    parts = signature_header.split(",")
    timestamp_part = parts.find { |p| p.start_with?("t=") }
    signature_part = parts.find { |p| p.start_with?("v1=") }

    return false unless timestamp_part && signature_part

    timestamp = timestamp_part[2..].to_i
    signature = signature_part[3..]

    # Check timestamp (reject if older than 5 minutes)
    now = Time.now.to_i
    if now - timestamp > 300
      puts "Webhook timestamp too old"
      return false
    end

    # Compute expected signature
    raw_body = request.body.read
    request.body.rewind
    signed_payload = "#{timestamp}.#{raw_body}"
    expected_signature = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)

    # Constant-time comparison
    Rack::Utils.secure_compare(expected_signature, signature)
  end

  post "/webhooks/zavu" do
    unless verify_zavu_signature(request, WEBHOOK_SECRET)
      puts "Invalid webhook signature"
      halt 401, "Invalid signature"
    end

    event = JSON.parse(request.body.read)
    puts "Verified webhook: #{event['type']}"

    status 200
    "OK"
  end
  ```

  ```go Go theme={null}
  package main

  import (
  	"crypto/hmac"
  	"crypto/sha256"
  	"encoding/hex"
  	"io"
  	"net/http"
  	"strconv"
  	"strings"
  	"time"
  )

  const webhookSecret = "whsec_your_secret_here" // From sender.webhook.secret

  func verifyZavuSignature(r *http.Request, secret string) bool {
  	signatureHeader := r.Header.Get("X-Zavu-Signature")
  	if signatureHeader == "" {
  		return false
  	}

  	// Parse the signature header
  	parts := strings.Split(signatureHeader, ",")
  	var timestamp int64
  	var signature string

  	for _, part := range parts {
  		if strings.HasPrefix(part, "t=") {
  			timestamp, _ = strconv.ParseInt(part[2:], 10, 64)
  		} else if strings.HasPrefix(part, "v1=") {
  			signature = part[3:]
  		}
  	}

  	if timestamp == 0 || signature == "" {
  		return false
  	}

  	// Check timestamp (reject if older than 5 minutes)
  	now := time.Now().Unix()
  	if now-timestamp > 300 {
  		return false
  	}

  	// Read body
  	body, _ := io.ReadAll(r.Body)

  	// Compute expected signature
  	signedPayload := strconv.FormatInt(timestamp, 10) + "." + string(body)
  	h := hmac.New(sha256.New, []byte(secret))
  	h.Write([]byte(signedPayload))
  	expectedSignature := hex.EncodeToString(h.Sum(nil))

  	// Constant-time comparison
  	return hmac.Equal([]byte(expectedSignature), []byte(signature))
  }

  func webhookHandler(w http.ResponseWriter, r *http.Request) {
  	if !verifyZavuSignature(r, webhookSecret) {
  		http.Error(w, "Invalid signature", http.StatusUnauthorized)
  		return
  	}

  	w.WriteHeader(http.StatusOK)
  	w.Write([]byte("OK"))
  }

  func main() {
  	http.HandleFunc("/webhooks/zavu", webhookHandler)
  	http.ListenAndServe(":3000", nil)
  }
  ```

  ```php PHP theme={null}
  <?php
  $webhookSecret = getenv('ZAVU_WEBHOOK_SECRET'); // From sender.webhook.secret

  function verifyZavuSignature(string $secret): bool {
      $signatureHeader = $_SERVER['HTTP_X_ZAVU_SIGNATURE'] ?? '';
      if (empty($signatureHeader)) {
          return false;
      }

      // Parse the signature header
      $parts = explode(',', $signatureHeader);
      $timestamp = null;
      $signature = null;

      foreach ($parts as $part) {
          if (str_starts_with($part, 't=')) {
              $timestamp = (int) substr($part, 2);
          } elseif (str_starts_with($part, 'v1=')) {
              $signature = substr($part, 3);
          }
      }

      if ($timestamp === null || $signature === null) {
          return false;
      }

      // Check timestamp (reject if older than 5 minutes)
      $now = time();
      if ($now - $timestamp > 300) {
          error_log('Webhook timestamp too old');
          return false;
      }

      // Compute expected signature
      $rawBody = file_get_contents('php://input');
      $signedPayload = "{$timestamp}.{$rawBody}";
      $expectedSignature = hash_hmac('sha256', $signedPayload, $secret);

      // Constant-time comparison
      return hash_equals($expectedSignature, $signature);
  }

  if (!verifyZavuSignature($webhookSecret)) {
      error_log('Invalid webhook signature');
      http_response_code(401);
      echo 'Invalid signature';
      exit;
  }

  $event = json_decode(file_get_contents('php://input'), true);
  error_log("Verified webhook: {$event['type']}");

  http_response_code(200);
  echo 'OK';
  ```
</CodeGroup>

## Timestamp Validation

Always validate the timestamp to prevent replay attacks:

```javascript theme={null}
const MAX_AGE_SECONDS = 300; // 5 minutes

function isTimestampValid(timestamp) {
  const now = Math.floor(Date.now() / 1000);
  return (now - timestamp) <= MAX_AGE_SECONDS;
}
```

<Warning>
  Never skip signature verification in production. An attacker could send fake webhook events to your endpoint.
</Warning>

## Troubleshooting

### Signature Mismatch

If signature verification fails:

1. **Check your secret** - Ensure you're using the correct webhook secret from the sender's webhook configuration
2. **Use raw body** - The signature is computed on the raw request body, not parsed JSON
3. **Check encoding** - Ensure the body is UTF-8 encoded
4. **Verify timestamp format** - The timestamp in the signature is in seconds, not milliseconds

### Testing Locally

For local development, you can temporarily disable signature verification or use a tool like [ngrok](https://ngrok.com) to expose your local server.

```javascript theme={null}
// Development only - never use in production!
const SKIP_VERIFICATION = process.env.NODE_ENV === 'development';

app.post('/webhooks/zavu', (req, res) => {
  if (!SKIP_VERIFICATION && !verifyZavuSignature(req, secret)) {
    return res.status(401).send('Invalid signature');
  }
  // ...
});
```

## Next Steps

* [Event Types](/guides/receiving-messages/events) - Understand webhook payloads
* [Webhooks](/guides/receiving-messages/webhooks) - Configure your endpoints
