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

# Webhook security

> Verify webhook signatures to prevent unauthorized requests

Always verify webhook signatures to ensure requests come from Kapso.

## Signature verification

Kapso signs all webhooks with HMAC SHA256 using your webhook secret key. The signature is included in the `X-Webhook-Signature` header.

### How it works

1. Kapso creates a signature by hashing the raw JSON payload with your secret key
2. The signature is sent in the `X-Webhook-Signature` header
3. Your endpoint recreates the signature using the same method
4. Compare signatures using a timing-safe comparison

### Node.js example

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

function verifyWebhook(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

app.post('/webhooks/whatsapp', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const isValid = verifyWebhook(
    req.body,
    signature,
    process.env.WEBHOOK_SECRET
  );

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  // Process webhook
  console.log('Event:', req.body.event);
  res.status(200).send('OK');
});
```

### Python example

```python theme={null}
import hmac
import hashlib
import json
from flask import Flask, request

app = Flask(__name__)

def verify_webhook(payload, signature, secret):
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        json.dumps(payload).encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_signature)

@app.route('/webhooks/whatsapp', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Webhook-Signature')
    is_valid = verify_webhook(
        request.json,
        signature,
        os.environ['WEBHOOK_SECRET']
    )

    if not is_valid:
        return 'Invalid signature', 401

    # Process webhook
    print('Event:', request.json['event'])
    return 'OK', 200
```

### Ruby example

```ruby theme={null}
require 'sinatra'
require 'json'
require 'openssl'

def verify_webhook(payload, signature, secret)
  expected_signature = OpenSSL::HMAC.hexdigest(
    'SHA256',
    secret,
    payload.to_json
  )

  Rack::Utils.secure_compare(signature, expected_signature)
end

post '/webhooks/whatsapp' do
  request.body.rewind
  payload = JSON.parse(request.body.read)
  signature = request.env['HTTP_X_WEBHOOK_SIGNATURE']

  unless verify_webhook(payload, signature, ENV['WEBHOOK_SECRET'])
    halt 401, 'Invalid signature'
  end

  # Process webhook
  puts "Event: #{payload['event']}"
  status 200
end
```

## Important notes

### Use the raw payload

Always verify against the raw JSON payload, not a parsed object:

```javascript theme={null}
// ❌ Wrong - verifying parsed object
verifyWebhook(req.body, signature, secret)

// ✅ Correct - verifying raw string
const rawBody = JSON.stringify(req.body);
verifyWebhook(rawBody, signature, secret)
```

### Use timing-safe comparison

Never use `===` or `==` to compare signatures. Use timing-safe comparison to prevent timing attacks:

```javascript theme={null}
// ❌ Wrong - vulnerable to timing attacks
if (signature === expectedSignature) { ... }

// ✅ Correct - timing-safe comparison
crypto.timingSafeEqual(
  Buffer.from(signature),
  Buffer.from(expectedSignature)
)
```

### Store secrets securely

* Never hardcode webhook secrets in your code
* Use environment variables or secret management services
* Rotate secrets periodically
* Use different secrets for development and production

## Idempotency

Webhooks may be delivered more than once. Use the `X-Idempotency-Key` header to track processed events.

### Simple in-memory tracking

```javascript theme={null}
const processedKeys = new Set();

app.post('/webhooks', (req, res) => {
  const idempotencyKey = req.headers['x-idempotency-key'];

  if (processedKeys.has(idempotencyKey)) {
    return res.status(200).send('Already processed');
  }

  // Process event
  processEvent(req.body);

  processedKeys.add(idempotencyKey);
  res.status(200).send('OK');
});
```

### Database-backed tracking

```javascript theme={null}
app.post('/webhooks', async (req, res) => {
  const idempotencyKey = req.headers['x-idempotency-key'];

  // Check if already processed
  const existing = await db.webhookEvents.findOne({
    idempotency_key: idempotencyKey
  });

  if (existing) {
    return res.status(200).send('Already processed');
  }

  // Process event
  await processEvent(req.body);

  // Store idempotency key
  await db.webhookEvents.create({
    idempotency_key: idempotencyKey,
    event: req.body.event,
    processed_at: new Date()
  });

  res.status(200).send('OK');
});
```

## Best practices

1. **Verify signatures first** - Before processing any webhook data
2. **Return 200 quickly** - Respond within 10 seconds to avoid retries
3. **Process asynchronously** - Use background jobs for heavy processing
4. **Handle duplicates** - Implement idempotency using `X-Idempotency-Key`
5. **Monitor failures** - Set up alerts for signature verification failures
6. **Use HTTPS only** - Never accept webhooks over HTTP
7. **Rotate secrets** - Change webhook secrets periodically
8. **Log everything** - Keep audit logs of webhook deliveries and failures
