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
- Get the raw request body (as bytes or UTF-8 string)
- Compute HMAC-SHA256 using your shared secret
- 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
)
endGo
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
- Check Secret: Ensure secret matches in destination config
- Verify Payload: Use raw request body, not parsed JSON
- Check Encoding: Ensure UTF-8 encoding for string payloads
- 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
- Webhooks Guide - Complete setup guide
- Webhook Payload Schema - Payload structure
- Webhook Configuration - Configuration options
Updated 29 days ago
