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

# Webhook security

> Verify RaaS Widgets webhook HMAC signatures using X-RAAS-* headers

## Why verify?

Your webhook URL is public. Signature verification proves the request came from RaaS Widgets, that the body was not altered, and (with a timestamp window) that it is not a stale replay.

<Warning>
  Always verify signatures before processing. Never trust the JSON body without a valid HMAC.
</Warning>

## Headers

Every delivery includes:

| Header             | Purpose              |
| ------------------ | -------------------- |
| `X-RAAS-Signature` | `sha256=` + hex HMAC |
| `X-RAAS-Timestamp` | Unix seconds         |
| `X-RAAS-Event`     | Event name           |

### Algorithm

```
signed_payload = "{timestamp}.{raw_json_body}"
signature = HMAC-SHA256(signed_payload, webhook_secret)
header = "sha256=" + hex(signature)
```

Use the **raw** body bytes (before JSON parse). Store the secret from the [Partner Portal](https://raas-widgets-partner-portal-sandbox.up.railway.app/) as something like:

```bash theme={null}
RAAS_WEBHOOK_SECRET=whsec_your_secret_here
```

## Verification steps

<Steps>
  <Step title="Read headers and raw body">
    Capture `X-RAAS-Signature`, `X-RAAS-Timestamp`, and the unmodified request body.
  </Step>

  <Step title="Compute expected HMAC">
    Sign `timestamp + "." + rawBody` with your webhook secret (SHA-256).
  </Step>

  <Step title="Compare safely">
    Use a constant-time compare against the hex digest (strip the `sha256=` prefix).
  </Step>

  <Step title="Reject stale timestamps">
    Optionally reject timestamps older than \~5 minutes to limit replay.
  </Step>
</Steps>

## Node.js (Express)

```javascript theme={null}
const express = require('express');
const crypto = require('crypto');

const app = express();
app.use('/webhooks/raas-widgets', express.raw({ type: 'application/json' }));

const WEBHOOK_SECRET = process.env.RAAS_WEBHOOK_SECRET;

function verifyWebhookSignature(req) {
  const signature = req.headers['x-raas-signature'];
  const timestamp = req.headers['x-raas-timestamp'];
  const rawBody = req.body.toString();

  if (!signature || !timestamp) return false;

  const ageMs = Math.abs(Date.now() - parseInt(timestamp, 10) * 1000);
  if (ageMs > 5 * 60 * 1000) return false;

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

  const received = signature.replace('sha256=', '');
  try {
    return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  } catch {
    return false;
  }
}

app.post('/webhooks/raas-widgets', (req, res) => {
  if (!verifyWebhookSignature(req)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body.toString());
  res.status(200).send('OK');
  // process event asynchronously…
});
```

## Python (Flask)

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

app = Flask(__name__)
WEBHOOK_SECRET = os.environ['RAAS_WEBHOOK_SECRET']

def verify_webhook_signature(req) -> bool:
    signature = req.headers.get('X-RAAS-Signature', '')
    timestamp = req.headers.get('X-RAAS-Timestamp', '')
    raw_body = req.get_data(as_text=True)
    if not signature or not timestamp:
        return False
    if abs(time.time() - int(timestamp)) > 5 * 60:
        return False
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        f'{timestamp}.{raw_body}'.encode(),
        hashlib.sha256,
    ).hexdigest()
    received = signature.replace('sha256=', '')
    return hmac.compare_digest(received, expected)

@app.post('/webhooks/raas-widgets')
def handle_webhook():
    if not verify_webhook_signature(request):
        abort(401)
    return 'OK', 200
```

## Common mistakes

<AccordionGroup>
  <Accordion title="Re-serializing JSON before verify">
    Parse only **after** HMAC check. Re-`JSON.stringify` can change whitespace/key order and break the signature.
  </Accordion>

  <Accordion title="Non-constant-time compare">
    Use `timingSafeEqual` / `compare_digest`, not `===`.
  </Accordion>

  <Accordion title="Skipping timestamp checks">
    Without a freshness window, captured payloads can be replayed.
  </Accordion>
</AccordionGroup>

## Regenerate the secret

1. Open the [Partner Portal](https://raas-widgets-partner-portal-sandbox.up.railway.app/)
2. Go to **Settings → Webhooks**
3. Regenerate the secret and deploy the new value to your servers

<Warning>
  The previous secret stops working immediately. Update your environment before or right after regenerating.
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="Events" icon="list" href="/raas-widgets/webhooks/events">
    Payload shapes for each event.
  </Card>

  <Card title="Overview" icon="webhook" href="/raas-widgets/webhooks/overview">
    Retries, timeouts, and configuration.
  </Card>
</CardGroup>
