Security (HMAC)
Webhooks using HMAC authentication — the default for new webhooks — sign every request so you can verify it came from Magnet Customer.
Important: Always validate the signature before processing any webhook payload. Reject requests with invalid or missing signatures.
How the signature is generated
We compute HMAC-SHA256 over the JSON-serialized request body using your webhook secret, then send the result in the X-Webhook-Signature header, formatted as sha256=<hex digest>. This header is only sent when the webhook's authentication type is HMAC with a secret configured — webhooks using Bearer, Basic, custom headers or no authentication do not receive it.
Validation in Node.js
const crypto = require('crypto');
function verifyWebhook(req) {
const signature = req.headers['x-webhook-signature'];
const secret = process.env.WEBHOOK_SECRET;
// Use raw body buffer — parsed JSON will break the signature
const hmac = crypto
.createHmac('sha256', secret)
.update(req.rawBody) // rawBody must be the original Buffer
.digest('hex');
const expected = `sha256=${hmac}`;
// Use timingSafeEqual to prevent timing attacks
const sigBuffer = Buffer.from(signature ?? '', 'utf8');
const expBuffer = Buffer.from(expected, 'utf8');
if (sigBuffer.length !== expBuffer.length) {
throw new Error('Invalid signature');
}
if (!crypto.timingSafeEqual(sigBuffer, expBuffer)) {
throw new Error('Invalid signature');
}
}
// Express example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
try {
verifyWebhook({ headers: req.headers, rawBody: req.body });
} catch {
return res.status(401).json({ error: 'Unauthorized' });
}
const event = JSON.parse(req.body.toString());
// Process event...
res.sendStatus(200);
});Validation in Python
import hmac
import hashlib
import os
def verify_webhook(raw_body: bytes, signature_header: str) -> bool:
"""Verify HMAC-SHA256 signature from Magnet Customer webhook."""
secret = os.environ["WEBHOOK_SECRET"].encode("utf-8")
expected = "sha256=" + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
# Use compare_digest to prevent timing attacks
return hmac.compare_digest(expected, signature_header)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def webhook():
signature = request.headers.get("X-Webhook-Signature", "")
if not verify_webhook(request.get_data(), signature):
abort(401)
event = request.json
# Process event...
return "", 200Headers sent with each request
X-Webhook-Signature is only present for HMAC-authenticated webhooks. The other headers below are sent on every delivery, regardless of authentication type.
| Header | Example value |
|---|---|
| X-Webhook-Signature | sha256=3ecb5a3d8f... |
| X-Webhook-Id | 65f2c1a4e8b9a400123abcde |
| X-Webhook-Event-Id | evt_1737054821123_k9d3f7a1x |
| X-Webhook-Timestamp | 1737054821123 |
| X-Webhook-Delivery-Attempt | 1 |
| User-Agent | MagnetCustomer-Webhook/1.0 |
Protecting against replay
The signature does not cover X-Webhook-Timestamp — timestamp and signature are independent, so a captured request could technically be replayed with its original signature still valid. As a defense-in-depth measure, we recommend rejecting deliveries whose X-Webhook-Timestamp (Unix epoch, in milliseconds) is older than a few minutes. This is a recommendation for your own integration, not a cryptographic guarantee provided by the signature.