← Webhooks

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

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

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 "", 200

Headers 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.

HeaderExample value
X-Webhook-Signaturesha256=3ecb5a3d8f...
X-Webhook-Id65f2c1a4e8b9a400123abcde
X-Webhook-Event-Idevt_1737054821123_k9d3f7a1x
X-Webhook-Timestamp1737054821123
X-Webhook-Delivery-Attempt1
User-AgentMagnetCustomer-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.