Docs / Webhook Security

Webhook Security

Verify webhook signatures for security

Webhook Security

Verify webhook signatures to ensure requests are from APIMW.

Signature Verification

Every webhook request includes a signature header:

X-APIMW-Signature: sha256=abc123...

How to Verify

1. Get the Raw Request Body

$payload = file_get_contents('php://input');

2. Compute the Expected Signature

$secret = 'your_webhook_secret';
$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);

3. Compare Signatures

$receivedSignature = $_SERVER['HTTP_X_APIMW_SIGNATURE'];

if (hash_equals($expectedSignature, $receivedSignature)) {
    // Valid webhook
} else {
    // Invalid - reject
    http_response_code(401);
    exit;
}

Code Examples

PHP

function verifyWebhookSignature($payload, $signature, $secret) {
    $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
    return hash_equals($expected, $signature);
}

$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_APIMW_SIGNATURE'] ?? '';

if (!verifyWebhookSignature($payload, $signature, $webhookSecret)) {
    http_response_code(401);
    die('Invalid signature');
}

$event = json_decode($payload, true);
// Process event...

Node.js

const crypto = require('crypto');

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

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

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-apimw-signature'];

  if (!verifySignature(req.rawBody, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  // Process event...
  res.status(200).send('OK');
});

Python

import hmac
import hashlib

def verify_signature(payload, signature, secret):
    expected = 'sha256=' + hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-APIMW-Signature', '')

    if not verify_signature(request.data.decode(), signature, WEBHOOK_SECRET):
        return 'Invalid signature', 401

    event = request.json
    # Process event...
    return 'OK', 200

Security Best Practices

  1. Always Verify - Never skip signature verification
  2. Use Timing-Safe Comparison - Prevent timing attacks
  3. Keep Secrets Secure - Store in environment variables
  4. Use HTTPS - Only accept webhooks over HTTPS
  5. Reject Invalid Requests - Return 401 for bad signatures