Webhook Verification

Webhook Verification

Sports Stack uses HMAC-SHA256 for webhook signature verification to ensure webhook requests are authentic and haven't been tampered with.

Signature Header

The signature is sent in the X-SportsStack-Signature header:

X-SportsStack-Signature: a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456

Verification Process

  1. Get the raw request body (as bytes or UTF-8 string)
  2. Compute HMAC-SHA256 using your shared secret
  3. Compare with the signature header using constant-time comparison

Code Examples

Python

import hmac
import hashlib

def verify_signature(payload_body, signature_header, secret):
    """
    Verify webhook signature.

    Args:
        payload_body: Raw request body (bytes or string)
        signature_header: X-SportsStack-Signature header value
        secret: Your shared secret

    Returns:
        bool: True if signature is valid
    """
    if isinstance(payload_body, str):
        payload_body = payload_body.encode('utf-8')

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

    return hmac.compare_digest(expected_signature, signature_header)

Node.js

const crypto = require('crypto');

function verifySignature(payloadBody, signatureHeader, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payloadBody)
    .digest('hex');

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

Ruby

require 'openssl'

def verify_signature(payload_body, signature_header, secret)
  expected_signature = OpenSSL::HMAC.hexdigest(
    'sha256',
    secret,
    payload_body
  )

  ActiveSupport::SecurityUtils.secure_compare(
    expected_signature,
    signature_header
  )
end

Go

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

func verifySignature(payloadBody []byte, signatureHeader, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payloadBody)
    expectedSignature := hex.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(expectedSignature), []byte(signatureHeader))
}

Security Best Practices

1. Always Verify Signatures

Never process webhooks without verifying the signature:

# ✅ Good
if verify_signature(request.body, signature_header, secret):
    process_webhook(payload)
else:
    return Response(status=401)

# ❌ Bad
process_webhook(payload)  # No verification!

2. Use Constant-Time Comparison

Always use constant-time comparison functions to prevent timing attacks:

  • Python: hmac.compare_digest()
  • Node.js: crypto.timingSafeEqual()
  • Ruby: ActiveSupport::SecurityUtils.secure_compare()
  • Go: hmac.Equal()

3. Store Secrets Securely

Never hardcode secrets in your code:

# ✅ Good
secret = os.environ.get('WEBHOOK_SECRET')

# ❌ Bad
secret = "my-secret-key"  # Hardcoded!

4. Validate Tenant ID

Always verify the tenant_id matches your expected tenant:

if payload['tenant_id'] != YOUR_TENANT_ID:
    return Response(status=403)

Troubleshooting

Signature Verification Failing

  1. Check Secret: Ensure secret matches in destination config
  2. Verify Payload: Use raw request body, not parsed JSON
  3. Check Encoding: Ensure UTF-8 encoding for string payloads
  4. Verify Algorithm: Sports Stack uses HMAC-SHA256

Common Mistakes

  • Using parsed JSON instead of raw body
  • Wrong encoding (not UTF-8)
  • Secret mismatch
  • Not using constant-time comparison

Related Documentation