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

# Security & Verification

> Verify webhook signatures to ensure authenticity

## Webhook Signatures

Every webhook request includes a signature that you should verify to ensure the request came from JetEmail and hasn't been tampered with.

## Signature Headers

Each webhook request includes these headers:

| Header                | Description                                  |
| --------------------- | -------------------------------------------- |
| `X-Webhook-ID`        | Unique identifier for this event             |
| `X-Webhook-Timestamp` | Unix timestamp when the event was sent       |
| `X-Webhook-Signature` | HMAC-SHA256 signature (format: `sha256=...`) |

## Verifying Signatures

The signature is computed using HMAC-SHA256 with your webhook secret and the raw request body.

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    const crypto = require('crypto');

    function verifyWebhookSignature(payload, signature, secret) {
      const expectedSignature = 'sha256=' + crypto
        .createHmac('sha256', secret)
        .update(payload)
        .digest('hex');
      
      return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
      );
    }

    // Express.js example
    app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
      const signature = req.headers['x-webhook-signature'];
      const payload = req.body;
      
      if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
        return res.status(401).send('Invalid signature');
      }
      
      const event = JSON.parse(payload);
      // Process the event...
      
      res.status(200).send('OK');
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import hmac
    import hashlib

    def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
        expected = 'sha256=' + hmac.new(
            secret.encode(),
            payload,
            hashlib.sha256
        ).hexdigest()
        
        return hmac.compare_digest(signature, expected)

    # Flask example
    from flask import Flask, request

    app = Flask(__name__)

    @app.route('/webhook', methods=['POST'])
    def webhook():
        signature = request.headers.get('X-Webhook-Signature')
        payload = request.get_data()
        
        if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
            return 'Invalid signature', 401
        
        event = request.get_json()
        # Process the event...
        
        return 'OK', 200
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    <?php

    function verifyWebhookSignature($payload, $signature, $secret) {
        $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
        return hash_equals($expected, $signature);
    }

    // Usage
    $payload = file_get_contents('php://input');
    $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
    $secret = getenv('WEBHOOK_SECRET');

    if (!verifyWebhookSignature($payload, $signature, $secret)) {
        http_response_code(401);
        exit('Invalid signature');
    }

    $event = json_decode($payload, true);
    // Process the event...

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

  <Tab title="Go">
    ```go theme={null}
    package main

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

    func verifyWebhookSignature(payload []byte, signature, secret string) bool {
        mac := hmac.New(sha256.New, []byte(secret))
        mac.Write(payload)
        expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
        return hmac.Equal([]byte(expected), []byte(signature))
    }

    func webhookHandler(w http.ResponseWriter, r *http.Request) {
        signature := r.Header.Get("X-Webhook-Signature")
        payload, _ := io.ReadAll(r.Body)
        
        if !verifyWebhookSignature(payload, signature, webhookSecret) {
            http.Error(w, "Invalid signature", http.StatusUnauthorized)
            return
        }
        
        // Process the event...
        
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("OK"))
    }
    ```
  </Tab>

  <Tab title="Ruby">
    ```ruby theme={null}
    require 'openssl'

    def verify_webhook_signature(payload, signature, secret)
      expected = 'sha256=' + OpenSSL::HMAC.hexdigest('sha256', secret, payload)
      Rack::Utils.secure_compare(expected, signature)
    end

    # Sinatra example
    post '/webhook' do
      payload = request.body.read
      signature = request.env['HTTP_X_WEBHOOK_SIGNATURE']
      
      unless verify_webhook_signature(payload, signature, ENV['WEBHOOK_SECRET'])
        halt 401, 'Invalid signature'
      end
      
      event = JSON.parse(payload)
      # Process the event...
      
      status 200
      'OK'
    end
    ```
  </Tab>
</Tabs>

## Security Best Practices

<CardGroup cols={2}>
  <Card title="Always Verify Signatures" icon="check">
    Never process webhook events without verifying the signature first. This prevents attackers from sending fake events to your endpoint.
  </Card>

  <Card title="Use HTTPS" icon="lock">
    Always use HTTPS for your webhook endpoint to ensure the payload is encrypted in transit.
  </Card>

  <Card title="Use Timing-Safe Comparison" icon="clock">
    Use constant-time string comparison functions to prevent timing attacks when verifying signatures.
  </Card>

  <Card title="Handle Duplicate Events" icon="copy">
    Use the `X-Webhook-ID` header to detect and handle duplicate events. Store processed event IDs and skip duplicates.
  </Card>
</CardGroup>

## Replay Prevention

To prevent replay attacks, you can verify that the timestamp is recent:

```javascript theme={null}
function isValidTimestamp(timestamp, toleranceSeconds = 300) {
  const now = Math.floor(Date.now() / 1000);
  const diff = Math.abs(now - timestamp);
  return diff <= toleranceSeconds;
}

// Usage
const timestamp = parseInt(req.headers['x-webhook-timestamp']);
if (!isValidTimestamp(timestamp)) {
  return res.status(401).send('Request too old');
}
```

<Tip>
  A tolerance of 5 minutes (300 seconds) is recommended to account for clock drift between servers.
</Tip>

## Rotating Secrets

If you need to rotate your webhook secret:

1. Generate a new secret in **Dashboard → Webhooks**
2. Update your application to accept both the old and new secrets
3. Once all events are using the new secret, remove the old one from your code
