Signature Verification
Every webhook delivery includes an HMAC-SHA256 signature in the X-BitByBit-Webhook-Signature header. Always verify this signature to ensure the request is genuinely from bitbybit.
X-BitByBit-Webhook-Signature: t=1700000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
t — Unix timestamp when the signature was generated
v1 — HMAC-SHA256 hex digest
How to Verify
The signature is computed as:
HMAC-SHA256(signing_secret, "{timestamp}.{raw_json_body}")
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(req, signingSecret) {
const signature = req.headers['x-bitbybit-webhook-signature'];
const [tPart, v1Part] = signature.split(',');
const timestamp = tPart.replace('t=', '');
const expectedSig = v1Part.replace('v1=', '');
// Prevent replay attacks — reject if older than 5 minutes
const currentTime = Math.floor(Date.now() / 1000);
if (currentTime - parseInt(timestamp) > 300) {
throw new Error('Webhook timestamp too old');
}
const payload = `${timestamp}.${JSON.stringify(req.body)}`;
const computedSig = crypto
.createHmac('sha256', signingSecret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(computedSig),
Buffer.from(expectedSig)
)) {
throw new Error('Invalid webhook signature');
}
return true;
}
Python
import hmac
import hashlib
import time
def verify_webhook_signature(request, signing_secret):
signature = request.headers.get('X-BitByBit-Webhook-Signature')
parts = dict(p.split('=', 1) for p in signature.split(','))
timestamp = parts['t']
expected_sig = parts['v1']
# Prevent replay attacks
if int(time.time()) - int(timestamp) > 300:
raise ValueError('Webhook timestamp too old')
payload = f"{timestamp}.{request.get_data(as_text=True)}"
computed_sig = hmac.new(
signing_secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(computed_sig, expected_sig):
raise ValueError('Invalid webhook signature')
return True
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
)
func VerifyWebhookSignature(signature, body, signingSecret string) error {
parts := strings.SplitN(signature, ",", 2)
timestamp := strings.TrimPrefix(parts[0], "t=")
expectedSig := strings.TrimPrefix(parts[1], "v1=")
// Prevent replay attacks
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if time.Now().Unix()-ts > 300 {
return fmt.Errorf("webhook timestamp too old")
}
payload := fmt.Sprintf("%s.%s", timestamp, body)
mac := hmac.New(sha256.New, []byte(signingSecret))
mac.Write([]byte(payload))
computedSig := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(computedSig), []byte(expectedSig)) {
return fmt.Errorf("invalid webhook signature")
}
return nil
}
Replay Attack Prevention
Always check the t (timestamp) value in the signature header. Reject any webhook where the timestamp is more than 5 minutes old. This prevents replay attacks where an attacker resends a previously captured webhook request.
Secret Rotation
You can rotate your signing secret at any time from Settings > Developer > Webhook Details > Rotate. When you rotate:
- A new signing secret is generated immediately
- The old secret is invalidated
- All subsequent deliveries use the new secret
- You must update your verification code with the new secret
Rotating a secret takes effect immediately. Make sure to update your server before rotating to avoid rejecting valid webhooks.