> ## Documentation Index
> Fetch the complete documentation index at: https://knowledge.bitbybit.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Security

> Verify webhook signatures to ensure authenticity

## 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

```javascript theme={null}
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

```python theme={null}
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

```go theme={null}
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

<Warning>
  Rotating a secret takes effect immediately. Make sure to update your server before rotating to avoid rejecting valid webhooks.
</Warning>
