Webhook security
All webhook requests include HMAC SHA256 signatures to ensure authenticity and integrity. You must verify these signatures to confirm that webhooks are genuinely from the service.
Signature Headers
Section titled “Signature Headers”Each webhook request includes these headers:
| Header | Description |
|---|---|
X-Timestamp |
Unix timestamp when the webhook was sent |
X-Signature |
HMAC SHA256 signature in format sha256={signature} |
Verification Process
Section titled “Verification Process”- Extract timestamp and signature from request headers
- Create signing string:
{timestamp}.{json_payload} - Generate HMAC SHA256 using your signing secret
- Compare generated signature with received signature
Implementation Examples
Section titled “Implementation Examples”Python
Section titled “Python”import hashlib
import hmac
import json
SIGNING_SECRET = "your-webhook-signing-secret"
def verify_webhook_signature(payload, timestamp, signature):
"""Verify webhook signature"""
# Create the signing string
signing_string = f"{timestamp}.{payload}"
# Generate expected signature
expected_signature = hmac.new(
SIGNING_SECRET.encode('utf-8'),
signing_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Remove 'sha256=' prefix from received signature
received_signature = signature.replace('sha256=', '')
# Compare signatures securely
return hmac.compare_digest(expected_signature, received_signature)
Node.js
Section titled “Node.js”const crypto = require('crypto');
const SIGNING_SECRET = 'your-webhook-signing-secret';
function verifyWebhookSignature(payload, timestamp, signature) {
// Create the signing string
const signingString = `${timestamp}.${payload}`;
// Generate expected signature
const expectedSignature = crypto
.createHmac('sha256', SIGNING_SECRET)
.update(signingString)
.digest('hex');
// Remove 'sha256=' prefix from received signature
const receivedSignature = signature.replace('sha256=', '');
// Compare signatures securely
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
<?php
function verifyWebhookSignature($payload, $timestamp, $signature, $secret) {
// Create the signing string
$signingString = $timestamp . '.' . $payload;
// Generate expected signature
$expectedSignature = hash_hmac('sha256', $signingString, $secret);
// Remove 'sha256=' prefix from received signature
$receivedSignature = str_replace('sha256=', '', $signature);
// Compare signatures securely
return hash_equals($expectedSignature, $receivedSignature);
}
?>
using System;
using System.Security.Cryptography;
using System.Text;
public class WebhookController
{
private readonly string _signingSecret = "your-webhook-signing-secret";
public bool VerifyWebhookSignature(string payload, string timestamp, string signature)
{
var signingString = $"{timestamp}.{payload}";
// Generate expected signature
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_signingSecret));
var expectedSignatureBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(signingString));
var expectedSignature = Convert.ToHexString(expectedSignatureBytes).ToLower();
// Remove 'sha256=' prefix from received signature
var receivedSignature = signature.Replace("sha256=", "").ToLower();
// Compare signatures
return expectedSignature == receivedSignature;
}
}
Security Best Practices
Section titled “Security Best Practices”Always Verify Signatures
Section titled “Always Verify Signatures”- Never process webhooks without signature verification
- Use timing-safe comparison functions to prevent timing attacks
- Validate both timestamp and signature headers
Additional Security Measures
Section titled “Additional Security Measures”- HTTPS Only: Only accept webhooks over HTTPS connections
- Timestamp Validation: Reject webhooks with timestamps too far in the past/future
- Payload Validation: Validate the structure and content of webhook payloads
- Rate Limiting: Implement rate limiting to prevent abuse
- IP Allowlisting: Consider restricting webhook sources to known IP ranges, our outgoing IP: 54.229.135.167
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”-
Signature Mismatch
- Verify your signing secret is correct
- Ensure JSON serialization matches exactly (no extra spaces)
- Check timestamp extraction from headers
-
Character Encoding
- Use UTF-8 encoding for all string operations
- Ensure consistent JSON serialization format
-
Header Case Sensitivity
- Headers may be case-insensitive depending on your framework
- Use case-insensitive header lookups when possible

