> ## 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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.