> ## 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 inbound webhook signatures to ensure authenticity

## Webhook Signatures

When you configure a **webhook secret** for your inbound domain, every webhook request is signed so you can verify it came from JetEmail and hasn't been tampered with.

## Signature Headers

Each signed webhook request includes these headers:

| Header                | Description                            |
| --------------------- | -------------------------------------- |
| `X-Webhook-ID`        | Unique job identifier                  |
| `X-Webhook-Timestamp` | Unix timestamp of the delivery attempt |
| `X-Webhook-Signature` | HMAC-SHA256 hex-encoded signature      |

## How the Signature is Computed

The signature is computed using HMAC-SHA256 with your webhook secret. The signing input combines the webhook ID, timestamp, and request body:

```
{webhook_id}.{timestamp}.{json_body}
```

The resulting hex-encoded digest is sent in the `X-Webhook-Signature` header.

## Verifying Signatures

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

    function verifyWebhookSignature(webhookId, timestamp, body, signature, secret) {
      const signingInput = `${webhookId}.${timestamp}.${body}`;
      const expectedSignature = crypto
        .createHmac('sha256', secret)
        .update(signingInput)
        .digest('hex');

      return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
      );
    }

    // Express.js example
    app.post('/inbound', express.raw({ type: 'application/json' }), (req, res) => {
      const webhookId = req.headers['x-webhook-id'];
      const timestamp = req.headers['x-webhook-timestamp'];
      const signature = req.headers['x-webhook-signature'];
      const body = req.body.toString();

      if (!verifyWebhookSignature(webhookId, timestamp, body, signature, process.env.WEBHOOK_SECRET)) {
        return res.status(401).send('Invalid signature');
      }

      const email = JSON.parse(body);
      // Process the inbound email...

      res.status(200).send('OK');
    });
    ```
  </Tab>

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

    def verify_webhook_signature(webhook_id: str, timestamp: str, body: bytes, signature: str, secret: str) -> bool:
        signing_input = f"{webhook_id}.{timestamp}.{body.decode()}"
        expected = hmac.new(
            secret.encode(),
            signing_input.encode(),
            hashlib.sha256
        ).hexdigest()

        return hmac.compare_digest(signature, expected)

    # Flask example
    from flask import Flask, request

    app = Flask(__name__)

    @app.route('/inbound', methods=['POST'])
    def inbound_webhook():
        webhook_id = request.headers.get('X-Webhook-ID')
        timestamp = request.headers.get('X-Webhook-Timestamp')
        signature = request.headers.get('X-Webhook-Signature')
        body = request.get_data()

        if not verify_webhook_signature(webhook_id, timestamp, body, signature, WEBHOOK_SECRET):
            return 'Invalid signature', 401

        email = request.get_json()
        # Process the inbound email...

        return 'OK', 200
    ```
  </Tab>

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

    function verifyWebhookSignature($webhookId, $timestamp, $body, $signature, $secret) {
        $signingInput = "{$webhookId}.{$timestamp}.{$body}";
        $expected = hash_hmac('sha256', $signingInput, $secret);
        return hash_equals($expected, $signature);
    }

    // Usage
    $body = file_get_contents('php://input');
    $webhookId = $_SERVER['HTTP_X_WEBHOOK_ID'] ?? '';
    $timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
    $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
    $secret = getenv('WEBHOOK_SECRET');

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

    $email = json_decode($body, true);
    // Process the inbound email...

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

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

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

    func verifyWebhookSignature(webhookID, timestamp string, body []byte, signature, secret string) bool {
        signingInput := fmt.Sprintf("%s.%s.%s", webhookID, timestamp, string(body))
        mac := hmac.New(sha256.New, []byte(secret))
        mac.Write([]byte(signingInput))
        expected := hex.EncodeToString(mac.Sum(nil))
        return hmac.Equal([]byte(expected), []byte(signature))
    }

    func inboundHandler(w http.ResponseWriter, r *http.Request) {
        webhookID := r.Header.Get("X-Webhook-ID")
        timestamp := r.Header.Get("X-Webhook-Timestamp")
        signature := r.Header.Get("X-Webhook-Signature")
        body, _ := io.ReadAll(r.Body)

        if !verifyWebhookSignature(webhookID, timestamp, body, signature, webhookSecret) {
            http.Error(w, "Invalid signature", http.StatusUnauthorized)
            return
        }

        // Process the inbound email...

        w.WriteHeader(http.StatusOK)
        w.Write([]byte("OK"))
    }
    ```
  </Tab>

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

    def verify_webhook_signature(webhook_id, timestamp, body, signature, secret)
      signing_input = "#{webhook_id}.#{timestamp}.#{body}"
      expected = OpenSSL::HMAC.hexdigest('sha256', secret, signing_input)
      Rack::Utils.secure_compare(expected, signature)
    end

    # Sinatra example
    post '/inbound' do
      body = request.body.read
      webhook_id = request.env['HTTP_X_WEBHOOK_ID']
      timestamp = request.env['HTTP_X_WEBHOOK_TIMESTAMP']
      signature = request.env['HTTP_X_WEBHOOK_SIGNATURE']

      unless verify_webhook_signature(webhook_id, timestamp, body, signature, ENV['WEBHOOK_SECRET'])
        halt 401, 'Invalid signature'
      end

      email = JSON.parse(body)
      # Process the inbound email...

      status 200
      'OK'
    end
    ```
  </Tab>
</Tabs>

## Security Best Practices

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

  <Card title="Use HTTPS" icon="lock">
    Always use HTTPS for your webhook endpoint to ensure email content 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="Validate the Timestamp" icon="calendar">
    Check that the `X-Webhook-Timestamp` is recent (within 5 minutes) to prevent replay attacks.
  </Card>
</CardGroup>

## Replay Prevention

To prevent replay attacks, 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 and retry delays.
</Tip>
