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

# Security & Signature Verification

> Verify webhook authenticity using HMAC-SHA256 signatures and protect against replay attacks

Every webhook Baanx sends is signed with HMAC-SHA256. Verifying this signature ensures the request is genuine and hasn't been tampered with.

## Your API Key

Each webhook endpoint is assigned a unique API key by Baanx. This key is the shared secret used to generate and verify signatures.

<Warning>
  Store your API key securely in a secrets manager or environment variable. Never log it, commit it to source control, or expose it in client-side code.
</Warning>

**Key properties:**

* Generated and managed by Baanx — not client-generated
* Unique per webhook endpoint
* Can be rotated on request (or via the [Rotate Key API](/api-reference/webhooks/rotate-key))
* Custom keys can be configured in special cases

**Example key format:**

```
whk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
```

## Request Headers

Every webhook request includes these headers:

| Header         | Description                                             |
| -------------- | ------------------------------------------------------- |
| `Content-Type` | Always `application/json`                               |
| `X-Timestamp`  | Unix timestamp (seconds, UTC) when the request was sent |
| `X-Signature`  | HMAC-SHA256 signature as a lowercase hex string         |

## How the Signature is Calculated

Baanx constructs the signature as follows:

```
signature_payload = X-Timestamp + "." + JSON.stringify(body)
signature         = HMAC-SHA256(api_key, signature_payload)
X-Signature       = hex_encode(signature)  // lowercase
```

To verify a request, you reproduce this calculation using the same inputs and compare the result to the `X-Signature` header.

<Warning>
  Use your **raw request body** (as a string, before JSON parsing) when computing the signature. Parsing and re-serialising the body can change whitespace and key ordering, causing verification to fail.
</Warning>

## Verification Steps

<Steps>
  <Step title="Extract headers">
    Read `X-Timestamp` and `X-Signature` from the incoming request headers.
  </Step>

  <Step title="Check the timestamp">
    Reject the request if the timestamp is more than **300 seconds (5 minutes)** away from the current server time. This prevents replay attacks.
  </Step>

  <Step title="Build the signature payload">
    Concatenate the timestamp, a literal `.`, and the raw JSON body string:

    ```
    payload = timestamp + "." + rawBody
    ```
  </Step>

  <Step title="Compute the expected signature">
    Calculate HMAC-SHA256 over the payload using your API key, then hex-encode the result (lowercase).
  </Step>

  <Step title="Compare signatures">
    Use a **timing-safe comparison** to compare the computed signature against `X-Signature`. Never use `===` — it is vulnerable to timing attacks.
  </Step>
</Steps>

## Code Examples

<CodeGroup>
  ```typescript TypeScript (Node.js) theme={null} theme={null}
  import crypto from 'crypto';

  function verifyWebhookSignature(
    rawBody: string,
    timestamp: string,
    signature: string,
    apiKey: string
  ): boolean {
    // Reject stale requests (> 5 minutes old)
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
      return false;
    }

    const payload = `${timestamp}.${rawBody}`;
    const expected = crypto
      .createHmac('sha256', apiKey)
      .update(payload)
      .digest('hex');

    // Use timing-safe comparison
    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signature)
    );
  }

  // Express.js usage — use raw body middleware
  app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
    const rawBody  = req.body.toString('utf8');
    const timestamp = req.headers['x-timestamp'] as string;
    const signature = req.headers['x-signature'] as string;

    if (!verifyWebhookSignature(rawBody, timestamp, signature, process.env.WEBHOOK_API_KEY!)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const event = JSON.parse(rawBody);
    // Process event...
    res.status(204).send();
  });
  ```

  ```python Python theme={null} theme={null}
  import hmac
  import hashlib
  import time
  from flask import Flask, request, jsonify

  def verify_webhook_signature(
      raw_body: str,
      timestamp: str,
      signature: str,
      api_key: str
  ) -> bool:
      # Reject stale requests (> 5 minutes old)
      now = int(time.time())
      if abs(now - int(timestamp)) > 300:
          return False

      payload = f"{timestamp}.{raw_body}"
      expected = hmac.new(
          api_key.encode('utf-8'),
          payload.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()

      # Use timing-safe comparison
      return hmac.compare_digest(expected, signature)

  # Flask usage — use request.get_data() for raw body
  app = Flask(__name__)

  @app.route('/webhooks', methods=['POST'])
  def handle_webhook():
      raw_body = request.get_data(as_text=True)
      timestamp = request.headers.get('X-Timestamp', '')
      signature = request.headers.get('X-Signature', '')

      if not verify_webhook_signature(raw_body, timestamp, signature, WEBHOOK_API_KEY):
          return jsonify({'error': 'Invalid signature'}), 401

      event = request.get_json()
      # Process event...
      return '', 204
  ```

  ```javascript JavaScript (Node.js) theme={null} theme={null}
  const crypto = require('crypto');

  function verifyWebhookSignature(rawBody, timestamp, signature, apiKey) {
    // Reject stale requests (> 5 minutes old)
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
      return false;
    }

    const payload = `${timestamp}.${rawBody}`;
    const expected = crypto
      .createHmac('sha256', apiKey)
      .update(payload)
      .digest('hex');

    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signature)
    );
  }
  ```
</CodeGroup>

## Replay Protection

The `X-Timestamp` header protects against replay attacks — where an attacker captures a legitimate webhook request and re-sends it later.

Your endpoint **should** reject any request where the timestamp differs from the current server time by more than **5 minutes (300 seconds)**:

```
| now - X-Timestamp | > 300  →  reject with 401
```

Ensure your server clock is synchronised (e.g., via NTP) so legitimate requests are not incorrectly rejected due to clock skew.

## Common Verification Mistakes

<AccordionGroup>
  <Accordion title="Parsing the body before verifying">
    If you parse the raw JSON into an object and then re-serialise it to compute the signature, the result may differ from the original raw string (different whitespace, key ordering). Always use the **raw request body string** from the HTTP request.
  </Accordion>

  <Accordion title="Using string equality instead of timing-safe comparison">
    Using `===` to compare signatures leaks information through response time differences. Always use `crypto.timingSafeEqual` (Node.js) or `hmac.compare_digest` (Python).
  </Accordion>

  <Accordion title="Not checking the timestamp">
    Without timestamp validation, a captured webhook request could be replayed indefinitely. Always reject requests where `| now - timestamp | > 300`.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Delivery & Retries" icon="truck-fast" href="/guides/webhooks/delivery">
    How to respond correctly and what happens when delivery fails
  </Card>

  <Card title="Rotate Signing Key" icon="arrows-rotate" href="/api-reference/webhooks/rotate-key">
    API reference for rotating your webhook API key
  </Card>
</CardGroup>
