Skip to main content

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.

Signature Format

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

Go

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:
  1. A new signing secret is generated immediately
  2. The old secret is invalidated
  3. All subsequent deliveries use the new secret
  4. 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.