Skip to content

Webhook Signature Verification

All webhook requests from Meta include an HMAC-SHA256 signature. CubeConnect verifies this signature automatically, but if you're building custom integrations, here's how it works.

How It Works

  1. Meta computes an HMAC-SHA256 hash of the request body using your app secret
  2. The hash is included in the X-Hub-Signature-256 header
  3. The receiver recomputes the hash and compares it to verify authenticity

Header Format

http
X-Hub-Signature-256: sha256=a1b2c3d4e5f6...

The header value starts with sha256= followed by the hex-encoded HMAC digest.

Verification Examples

php
function verifySignature(string $payload, string $signature, string $appSecret): bool
{
    $expected = hash_hmac('sha256', $payload, $appSecret);
    $received = str_replace('sha256=', '', $signature);

    return hash_equals($expected, $received);
}

// Usage
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
$appSecret = env('WHATSAPP_APP_SECRET');

if (!verifySignature($payload, $signature, $appSecret)) {
    http_response_code(401);
    exit('Invalid signature');
}
javascript
const crypto = require('crypto')

function verifySignature(payload, signature, appSecret) {
  const expected = crypto
    .createHmac('sha256', appSecret)
    .update(payload)
    .digest('hex')

  const received = signature.replace('sha256=', '')

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(received)
  )
}

// Usage in Express
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-hub-signature-256'] || ''
  const payload = JSON.stringify(req.body)

  if (!verifySignature(payload, signature, process.env.WHATSAPP_APP_SECRET)) {
    return res.status(401).send('Invalid signature')
  }

  // Process webhook...
  res.sendStatus(200)
})
python
import hmac
import hashlib

def verify_signature(payload: bytes, signature: str, app_secret: str) -> bool:
    expected = hmac.new(
        app_secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    received = signature.replace('sha256=', '')

    return hmac.compare_digest(expected, received)

# Usage in Flask
@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Hub-Signature-256', '')
    payload = request.get_data()

    if not verify_signature(payload, signature, os.environ['WHATSAPP_APP_SECRET']):
        return 'Invalid signature', 401

    # Process webhook...
    return '', 200

Important Notes

  • Always use time-safe comparison (hash_equals in PHP, timingSafeEqual in Node.js, compare_digest in Python) to prevent timing attacks
  • Use the raw request body for signature computation, not a parsed/re-serialized version
  • The app secret is configured per WhatsApp account and can be found in your Meta App settings
  • CubeConnect handles signature verification automatically for webhooks sent to the platform

CubeConnect WhatsApp Business Platform