m2pfintech
Webhook Guide

Signature Verification

Verify webhook authenticity using HMAC-SHA256 signatures to prevent spoofing.

Every webhook delivery includes an HMAC-SHA256 signature in the x-m2p-signature header. Always verify this signature before processing any webhook event.


How Verification Works


Implementation

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = 'sha256=' +
    crypto.createHmac('sha256', secret)
      .update(JSON.stringify(payload))
      .digest('hex');

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

// Express.js webhook handler
app.post('/webhook', (req, res) => {
  const isValid = verifyWebhookSignature(
    req.body,
    req.headers['x-m2p-signature'],
    process.env.WEBHOOK_SECRET
  );

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

  // Process the event
  handleEvent(req.body);
  res.status(200).send('OK');
});
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;

public class WebhookVerifier {

    public static boolean verifySignature(
        String payload, String signature, String secret
    ) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(
            secret.getBytes(), "HmacSHA256"
        ));

        String expected = "sha256=" +
            bytesToHex(mac.doFinal(payload.getBytes()));

        return MessageDigest.isEqual(
            expected.getBytes(),
            signature.getBytes()
        );
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}
import hmac
import hashlib

def verify_webhook_signature(payload: bytes,
                              signature: str,
                              secret: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

# Flask webhook handler
@app.route('/webhook', methods=['POST'])
def webhook():
    is_valid = verify_webhook_signature(
        request.data,
        request.headers.get('x-m2p-signature', ''),
        os.environ['WEBHOOK_SECRET']
    )

    if not is_valid:
        return 'Invalid signature', 401

    handle_event(request.json)
    return 'OK', 200

Always use constant-time comparison (e.g., timingSafeEqual, MessageDigest.isEqual, hmac.compare_digest) to prevent timing attacks. Never use simple string equality (==).

On this page