Skip to content

Webhook Signature Verification

CubeConnect signs outbound webhook requests using HMAC-SHA256 when you configure a signing secret. This allows you to verify that webhook requests genuinely come from CubeConnect.

Outbound Webhook Signatures

When you set a Signing Secret in Settings → Webhook, every outbound webhook includes two headers:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 hex digest
X-Webhook-TimestampISO 8601 timestamp used in the signature

How It Works

  1. CubeConnect creates a string: {timestamp}.{json_payload}
  2. Computes HMAC-SHA256 using your signing secret
  3. Sends the hex digest in the X-Webhook-Signature header

Verification Examples

php
function verifyWebhookSignature(Request $request, string $secret): bool
{
    $signature = $request->header('X-Webhook-Signature');
    $timestamp = $request->header('X-Webhook-Timestamp');

    if (!$signature || !$timestamp) {
        return false;
    }

    // Prevent replay attacks (5-minute tolerance)
    $requestTime = strtotime($timestamp);
    if (abs(time() - $requestTime) > 300) {
        return false;
    }

    $payload = $request->getContent();
    $expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);

    return hash_equals($expected, $signature);
}

// Usage in Laravel controller
public function handleWebhook(Request $request)
{
    $secret = config('services.cubeconnect.webhook_secret');

    if (!verifyWebhookSignature($request, $secret)) {
        return response('Invalid signature', 401);
    }

    $event = $request->input('event');
    // Process webhook...

    return response('OK', 200);
}
javascript
const crypto = require('crypto')

function verifyWebhookSignature(body, signature, timestamp, secret) {
  if (!signature || !timestamp) return false

  // Prevent replay attacks (5-minute tolerance)
  const requestTime = new Date(timestamp).getTime()
  if (Math.abs(Date.now() - requestTime) > 300000) return false

  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + body)
    .digest('hex')

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

// Usage in Express
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature']
  const timestamp = req.headers['x-webhook-timestamp']
  const body = req.body.toString()

  if (!verifyWebhookSignature(body, signature, timestamp, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature')
  }

  const payload = JSON.parse(body)
  console.log('Event:', payload.event)
  // Process webhook...

  res.sendStatus(200)
})
python
import hmac
import hashlib
from datetime import datetime, timezone

def verify_webhook_signature(body: str, signature: str, timestamp: str, secret: str) -> bool:
    if not signature or not timestamp:
        return False

    # Prevent replay attacks (5-minute tolerance)
    request_time = datetime.fromisoformat(timestamp)
    now = datetime.now(timezone.utc)
    if abs((now - request_time).total_seconds()) > 300:
        return False

    expected = hmac.new(
        secret.encode(),
        (timestamp + '.' + body).encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

# Usage in Flask
@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Webhook-Signature', '')
    timestamp = request.headers.get('X-Webhook-Timestamp', '')
    body = request.get_data(as_text=True)

    if not verify_webhook_signature(body, signature, timestamp, os.environ['WEBHOOK_SECRET']):
        return 'Invalid signature', 401

    payload = request.get_json()
    print(f"Event: {payload['event']}")
    # 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
  • Check the timestamp to prevent replay attacks (reject requests older than 5 minutes)
  • The signing secret is configured in Settings → Webhook and can be regenerated at any time

Meta Webhook Signatures (Inbound)

Meta signs webhook requests sent to CubeConnect using a different scheme. This is handled automatically by CubeConnect, but for reference:

Header Format

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

Verification

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

    return hash_equals($expected, $received);
}

INFO

CubeConnect handles Meta signature verification automatically. You only need to verify signatures on your outbound webhook endpoint using the examples above.

CubeConnect WhatsApp Business Platform