Skip to content

Webhook Verification

MXHook signs webhook payloads with HMAC-SHA256 so your application can verify they're authentic.

How It Works

When a route has a webhook_secret configured, MXHook:

  1. Serializes the email payload as JSON
  2. Computes an HMAC-SHA256 hash using the route's secret as the key
  3. Includes the signature in the X-MXHook-Signature header

The header value is prefixed with sha256=:

X-MXHook-Signature: sha256=a1b2c3d4e5f6...

Verifying in Your Application

Node.js

javascript
const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

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

// Express example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-mxhook-signature'];
  const isValid = verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const email = JSON.parse(req.body);
  // Process the email...
  res.status(200).send('OK');
});

Python

python
import hmac
import hashlib

def verify_webhook(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
@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-MXHook-Signature', '')
    is_valid = verify_webhook(request.data, signature, os.environ['WEBHOOK_SECRET'])

    if not is_valid:
        return 'Invalid signature', 401

    email = request.get_json()
    # Process the email...
    return 'OK', 200

Go

go
func verifyWebhook(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(signature), []byte(expected))
}

Security Notes

  • Always use timingSafeEqual (or equivalent) to compare signatures. String comparison with == is vulnerable to timing attacks.
  • Use HTTPS for your webhook endpoint to prevent payload interception.
  • Reject unsigned requests if you expect all payloads to be signed.
  • Rotate secrets by creating a new route with a new secret and deleting the old one.

Released under the Apache 2.0 License.