HMAC Signature Validation

Complete guide to webhooks, debugging, and real-time event monitoring with HookMetry

Custom HMAC SHA256 Webhooks

HMAC SHA256 is the industry-standard method for securing webhooks. Use this for custom integrations or APIs that don't match Stripe/GitHub/Shopify formats.

How HMAC SHA256 Works

HMAC (Hash-based Message Authentication Code) combines a secret key with your webhook payload to create a unique signature:

signature = HMAC-SHA256(secret_key, request_body)
  1. Sender: Computes HMAC of payload using shared secret
  2. Sender: Includes signature in HTTP header
  3. Receiver: Recomputes HMAC using same secret
  4. Receiver: Compares signatures to verify authenticity

Supported Header Names

Hookmetry automatically checks these common header names:

X-SignatureX-Webhook-SignatureX-Hub-SignatureX-HMAC-Signature

Creating HMAC Signatures

Node.js — Sender Side (you control the sender)

const crypto = require('crypto');
const axios = require('axios');

// SENDER SIDE: You are the one sending the webhook.
// You own both the payload and the secret.
const payload = JSON.stringify({ event: 'user.created', userId: 123 });
const secret  = process.env.WEBHOOK_SECRET;

// Hash the serialized string
const signature = crypto
  .createHmac('sha256', secret)
  .update(payload)  // ← string here is fine — YOU control the format
  .digest('hex');

await axios.post('https://api.hookmetry.com/webhook/ep_YOUR_ENDPOINT_ID', payload, {
  headers: {
    'Content-Type': 'application/json',
    'X-Signature': signature
  }
});

// ⚠️ RECEIVER SIDE is different — see "Verifying" section below.

Python — Sender Side

import hmac, hashlib, json, os
import requests

# SENDER SIDE: serialize first, then hash the string
payload_str = json.dumps({'event': 'user.created', 'userId': 123})
secret = os.environ['WEBHOOK_SECRET'].encode('utf-8')

signature = hmac.new(secret, payload_str.encode('utf-8'), hashlib.sha256).hexdigest()

requests.post(
    'https://api.hookmetry.com/webhook/ep_YOUR_ENDPOINT_ID',
    data=payload_str,
    headers={
        'Content-Type': 'application/json',
        'X-Signature': signature
    }
)

PHP

<?php
function generateHMAC($payload, $secret) {
    $json = json_encode($payload);
    $signature = hash_hmac('sha256', $json, $secret);
    return $signature;
}

// Usage
$payload = ['event' => 'user.created', 'userId' => 123];
$secret = 'your_webhook_secret';
$signature = generateHMAC($payload, $secret);

// Send webhook
$ch = curl_init('https://api.hookmetry.com/webhook/ep_YOUR_ENDPOINT_ID');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'X-Signature: ' . $signature
]);
curl_exec($ch);
?>

Verifying HMAC Signatures — Receiver Side (Your Server)

The receiver side is fundamentally different: you must hash the raw request body bytes, not a re-serialized JSON object. Parsing and re-stringifying will change whitespace and field order, breaking the signature.

const crypto = require('crypto');
const express = require('express');
const app = express();

// CRITICAL: express.raw() — not express.json() — for webhook routes.
// express.json() parses the body and loses the raw bytes Hookmetry signed.
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const receivedSig = req.headers['x-signature'];
  if (!receivedSig) return res.status(400).send('Missing signature header');

  // req.body is a raw Buffer — hash it directly
  const expectedSig = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(req.body)          // ← raw Buffer, NOT JSON.stringify(req.body)
    .digest('hex');

  // Timing-safe comparison
  const a = Buffer.from(receivedSig, 'hex');
  const b = Buffer.from(expectedSig, 'hex');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Safe to parse now
  const event = JSON.parse(req.body);
  console.log('Valid event:', event.type);
  res.status(200).json({ received: true });
});

Security Best Practices:

  • • Use long, randomly-generated secrets (32+ characters)
  • • Never hardcode secrets in your code - use environment variables
  • • Use timing-safe comparison functions to prevent timing attacks
  • • Rotate secrets periodically (every 90 days recommended)
  • • Always use HTTPS to prevent man-in-the-middle attacks

Common HMAC Pitfalls

Mismatched Payload Serialization

The sender and receiver must serialize the payload identically. Watch out for:

  • • Different JSON key ordering
  • • Extra whitespace or newlines
  • • Different encoding (UTF-8 vs ASCII)

Using Raw Body vs Parsed JSON

If you parse JSON before verification, you may alter the payload. Always verify against the raw body.

Case Sensitivity Issues

HMAC signatures are case-sensitive. Don't convert to uppercase/lowercase.

Testing HMAC Webhooks:

Create a Custom HMAC endpoint in Hookmetry, set your secret, then use the code examples above to send test webhooks. Check the webhook logs to see validation results and debug signature mismatches.

Was this page helpful?

Your feedback helps us improve the docs.