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

# Verifying Webhook Signatures

> Learn how to verify HMAC-SHA256 signatures on incoming webhooks to ensure they came from KnoxCall and weren't tampered with.

# Verifying Webhook Signatures

Every webhook from KnoxCall includes an HMAC-SHA256 signature. Verifying this signature ensures the webhook:

1. **Came from KnoxCall** (not an attacker)
2. **Wasn't modified** in transit

## How Signatures Work

When KnoxCall sends a webhook:

1. Takes the JSON payload
2. Signs it with your webhook's secret key using HMAC-SHA256
3. Includes the signature in the `X-Webhook-Signature` header

```text theme={"dark"}
X-Webhook-Signature: sha256=a1b2c3d4e5f6...
```

Your endpoint should:

1. Get the signature from the header
2. Compute the expected signature using the same secret
3. Compare them (timing-safe comparison)
4. Reject if they don't match

## Implementation Examples

### Node.js (Express)

```javascript theme={"dark"}
const crypto = require('crypto');
const express = require('express');
const app = express();

// IMPORTANT: Use raw body for signature verification
app.use('/webhook', express.raw({ type: 'application/json' }));

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

function verifySignature(payload, signature) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');

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

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-webhook-signature'];

  if (!signature) {
    return res.status(401).json({ error: 'Missing signature' });
  }

  if (!verifySignature(req.body, signature)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Signature valid - process the webhook
  const event = JSON.parse(req.body);
  console.log('Received event:', event.event);

  res.status(200).json({ received: true });
});

app.listen(3000);
```

### Python (Flask)

```python theme={"dark"}
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')

def verify_signature(payload, signature):
    expected = 'sha256=' + hmac.new(
        WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Webhook-Signature')

    if not signature:
        return jsonify({'error': 'Missing signature'}), 401

    if not verify_signature(request.data, signature):
        return jsonify({'error': 'Invalid signature'}), 401

    # Signature valid - process the webhook
    event = request.json
    print(f"Received event: {event['event']}")

    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(port=3000)
```

### PHP

```php theme={"dark"}
<?php

$webhookSecret = getenv('WEBHOOK_SECRET');
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

function verifySignature($payload, $signature, $secret) {
    $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
    return hash_equals($expected, $signature);
}

if (empty($signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Missing signature']);
    exit;
}

if (!verifySignature($payload, $signature, $webhookSecret)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Signature valid - process the webhook
$event = json_decode($payload, true);
error_log("Received event: " . $event['event']);

http_response_code(200);
echo json_encode(['received' => true]);
```

### Go

```go theme={"dark"}
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io"
    "net/http"
    "os"
)

var webhookSecret = os.Getenv("WEBHOOK_SECRET")

func verifySignature(payload []byte, signature string) bool {
    mac := hmac.New(sha256.New, []byte(webhookSecret))
    mac.Write(payload)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-Webhook-Signature")

    if signature == "" {
        http.Error(w, `{"error":"Missing signature"}`, http.StatusUnauthorized)
        return
    }

    payload, _ := io.ReadAll(r.Body)

    if !verifySignature(payload, signature) {
        http.Error(w, `{"error":"Invalid signature"}`, http.StatusUnauthorized)
        return
    }

    // Signature valid - process the webhook
    var event map[string]interface{}
    json.Unmarshal(payload, &event)

    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`{"received":true}`))
}

func main() {
    http.HandleFunc("/webhook", webhookHandler)
    http.ListenAndServe(":3000", nil)
}
```

### Ruby (Sinatra)

```ruby theme={"dark"}
require 'sinatra'
require 'json'
require 'openssl'

WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']

def verify_signature(payload, signature)
  expected = 'sha256=' + OpenSSL::HMAC.hexdigest(
    'sha256',
    WEBHOOK_SECRET,
    payload
  )
  Rack::Utils.secure_compare(expected, signature)
end

post '/webhook' do
  request.body.rewind
  payload = request.body.read
  signature = request.env['HTTP_X_WEBHOOK_SIGNATURE']

  unless signature
    halt 401, { error: 'Missing signature' }.to_json
  end

  unless verify_signature(payload, signature)
    halt 401, { error: 'Invalid signature' }.to_json
  end

  # Signature valid - process the webhook
  event = JSON.parse(payload)
  puts "Received event: #{event['event']}"

  { received: true }.to_json
end
```

## Getting Your Webhook Secret

### View Secret

1. Click **Automation** in the sidebar, then select **Webhooks**
2. Click on your webhook
3. Click **Reveal Secret**
4. Copy the secret

The secret looks like: `a1b2c3d4e5f6...` (64 hex characters)

### Regenerate Secret

If your secret is compromised:

1. Click **Regenerate Secret**
2. Copy the new secret immediately
3. Update your endpoint with the new secret

<Warning>
  **Regenerating invalidates the old secret immediately.** Update your endpoint before regenerating, or webhooks will fail validation.
</Warning>

## Common Mistakes

### 1. Parsing JSON Before Verification

**Wrong:**

```javascript theme={"dark"}
// DON'T DO THIS
app.use(express.json());

app.post('/webhook', (req, res) => {
  // req.body is already parsed - signature will fail!
  const signature = computeSignature(JSON.stringify(req.body));
});
```

**Right:**

```javascript theme={"dark"}
// Use raw body for webhook routes
app.use('/webhook', express.raw({ type: 'application/json' }));

app.post('/webhook', (req, res) => {
  // req.body is the raw string
  const signature = computeSignature(req.body);
});
```

### 2. Wrong Secret

**Symptoms:**

* All signatures fail
* No intermittent issues

**Fix:**

* Copy secret directly from KnoxCall
* Ensure no extra whitespace
* Check environment variable is set

### 3. Non-Timing-Safe Comparison

**Wrong:**

```javascript theme={"dark"}
// DON'T DO THIS - vulnerable to timing attacks
if (signature === expected) { ... }
```

**Right:**

```javascript theme={"dark"}
// Use timing-safe comparison
if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { ... }
```

### 4. Encoding Issues

**Symptoms:**

* Signatures fail for certain payloads
* Works for simple data, fails for special characters

**Fix:**

* Use raw bytes, not decoded strings
* Ensure UTF-8 encoding throughout

## Security Best Practices

### 1. Always Verify Signatures

Never process webhooks without signature verification:

```javascript theme={"dark"}
if (!verifySignature(payload, signature)) {
  // Log the attempt for security monitoring
  console.warn('Invalid webhook signature from:', req.ip);
  return res.status(401).json({ error: 'Invalid signature' });
}
```

### 2. Store Secrets Securely

* Use environment variables (not hardcoded)
* Rotate secrets periodically
* Use secrets management services in production

### 3. Reject Missing Signatures

```javascript theme={"dark"}
if (!signature) {
  return res.status(401).json({ error: 'Missing signature' });
}
```

### 4. Log Verification Failures

Monitor for repeated failures - could indicate an attack:

```javascript theme={"dark"}
if (!verifySignature(payload, signature)) {
  logger.warn({
    message: 'Webhook signature verification failed',
    ip: req.ip,
    webhookId: req.headers['x-webhook-id']
  });
}
```

### 5. Use HTTPS Only

Signatures protect against tampering, but HTTPS protects against eavesdropping:

* Always use HTTPS endpoints
* Reject HTTP in production

## Debugging Signature Issues

### Step 1: Log Both Signatures

```javascript theme={"dark"}
console.log('Received:', signature);
console.log('Expected:', expected);
```

### Step 2: Check Payload

```javascript theme={"dark"}
console.log('Payload length:', payload.length);
console.log('Payload:', payload.toString());
```

### Step 3: Verify Secret

```javascript theme={"dark"}
console.log('Secret length:', WEBHOOK_SECRET.length);
console.log('Secret first 4 chars:', WEBHOOK_SECRET.substring(0, 4));
```

### Step 4: Test with Known Values

Use the KnoxCall test feature and compare:

* Test payload vs what you receive
* Expected signature vs what you compute

## Next Steps

<CardGroup cols={2}>
  <Card title="Testing Webhooks" icon="flask" href="/webhooks/testing-webhooks">
    Test your signature verification
  </Card>

  <Card title="Webhook Logs" icon="list" href="/webhooks/webhook-logs">
    Debug webhook issues
  </Card>

  <Card title="Creating Webhooks" icon="plus" href="/webhooks/creating-webhooks">
    Configure webhook settings
  </Card>

  <Card title="Webhooks Overview" icon="webhook" href="/webhooks/webhooks-overview">
    Learn about webhooks
  </Card>
</CardGroup>

***

<CardGroup cols={2}>
  <Card title="Statistics" icon="chart-line">
    * **Level**: intermediate
    * **Time**: 8 minutes
  </Card>

  <Card title="Tags" icon="tags">
    `webhooks`, `security`, `signatures`, `hmac`, `verification`
  </Card>
</CardGroup>
