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

# Verifying Signatures

> Validate that webhook payloads came from Bodha using HMAC-SHA256.

Every webhook delivery includes an `X-Bodha-Signature` header containing
an HMAC-SHA256 hex digest of the raw request body, signed with your
endpoint's secret.

**Always verify the signature** before processing a webhook — it proves
the payload was sent by Bodha and hasn't been tampered with.

## How it works

1. When you create a webhook endpoint, Bodha generates a secret starting
   with `whsec_`.
2. For each delivery, Bodha computes `HMAC-SHA256(secret, rawBody)` and
   sends the hex digest in `X-Bodha-Signature`.
3. Your server recomputes the same HMAC and compares it using a
   timing-safe comparison.

## Verification examples

```javascript Node.js theme={null}
import crypto from 'crypto';

function verifyWebhook(req, secret) {
  const signature = req.headers['x-bodha-signature'];
  const expected = crypto
    .createHmac('sha256', secret)
    .update(req.rawBody) // use the raw body, not parsed JSON
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex'),
  );
}

// Express example
app.post('/webhooks/bodha', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyWebhook(req, process.env.BODHA_WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  console.log(`Received ${event.event}`, event.data);
  res.sendStatus(200);
});
```

```python Python theme={null}
import hmac
import hashlib

def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# Flask example
@app.route('/webhooks/bodha', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Bodha-Signature', '')
    if not verify_webhook(request.data, signature, WEBHOOK_SECRET):
        return 'Invalid signature', 401

    event = request.get_json()
    print(f"Received {event['event']}", event['data'])
    return '', 200
```

```ruby Ruby theme={null}
require 'openssl'

def verify_webhook(payload, signature, secret)
  expected = OpenSSL::HMAC.hexdigest('SHA256', secret, payload)
  Rack::Utils.secure_compare(expected, signature)
end
```

## Important notes

> [!WARNING]
>
> **Use the raw request body** — not the parsed and re-serialized JSON.
> Parsing and re-serializing can change key order or whitespace, which
> will produce a different HMAC.

* **Timing-safe comparison** — Always use `crypto.timingSafeEqual` (Node),
  `hmac.compare_digest` (Python), or `Rack::Utils.secure_compare` (Ruby)
  to prevent timing attacks.
* **Rolling secrets** — You can roll (rotate) an endpoint's secret from the
  Developer Portal or via the API. After rolling, only the new secret
  signs future deliveries.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.