Webhook Signature Validation
Signature Format
Webhooks use versioned signatures for secret rotation:
X-Signature: t=timestamp,v1=current_sig,v0=old_sig
Components:
t: Unix timestamp when sentv1: Current signature (HMAC SHA256)v0: Expiring signature (during rotation only)
Validation Algorithm
signed_payload = timestamp + "." + request_body
expected_sig = HMAC-SHA256(signed_payload, webhook_secret)
Verify:
- Parse timestamp and v1 from header
- Check timestamp (reject if > 5 minutes)
- Compute expected signature
- Constant-time compare with v1
- During rotation, also check v0
PHP Implementation
function verifyWebhook($payload, $signatureHeader, $secret) {
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
[$key, $value] = explode('=', $part, 2);
$parts[$key] = $value;
}
if (abs(time() - (int)$parts['t']) > 300) {
return false;
}
$signedPayload = $parts['t'] . '.' . $payload;
$expectedSig = hash_hmac('sha256', $signedPayload, $secret);
return hash_equals($expectedSig, $parts['v1'])
|| (isset($parts['v0']) && hash_equals($expectedSig, $parts['v0']));
}
JavaScript Implementation
function verifyWebhook(payload, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
if (Math.abs(Date.now()/1000 - parseInt(parts.t)) > 300) {
return false;
}
const signedPayload = `${parts.t}.${payload}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expectedSig), Buffer.from(parts.v1))
|| (parts.v0 && crypto.timingSafeEqual(Buffer.from(expectedSig), Buffer.from(parts.v0)));
}
Python Implementation
def verify_webhook(payload: str, signature_header: str, secret: str) -> bool:
parts = dict(p.split('=', 1) for p in signature_header.split(','))
if abs(time.time() - int(parts['t'])) > 300:
return False
signed_payload = f"{parts['t']}.{payload}"
expected_sig = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_sig, parts['v1']) \
or ('v0' in parts and hmac.compare_digest(expected_sig, parts['v0']))