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:
- Serializes the email payload as JSON
- Computes an HMAC-SHA256 hash using the route's secret as the key
- Includes the signature in the
X-MXHook-Signatureheader
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', 200Go
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.