# Webhook Security

> Learn how to securely receive and verify webhook notifications from RevRag.ai

URL: /api-reference/webhook-security
Markdown: /api-reference/webhook-security.md



## Overview [#overview]

RevRag.ai sends webhook notifications to your configured endpoint when specific events occur (such as when a call ends). To ensure the security and authenticity of these webhooks, we implement cryptographic signature verification that allows you to confirm that requests are genuinely from RevRag.ai.

## Authentication Method [#authentication-method]

We use **HMAC-based authentication** to secure webhook notifications. This method uses a shared secret to cryptographically sign webhook payloads, providing strong security guarantees for verifying the authenticity and integrity of each request.

## Configuration [#configuration]

To set up webhooks, you'll need to configure:

* **Webhook URL**: The HTTPS endpoint where you'll receive webhook notifications (HTTP is not supported for security reasons)
* **Secret Key**: A secure shared secret used for HMAC signature verification
* **Event Types**: Select which events you want to receive (currently supported: `call.ended`)

Contact our support team at &#x2A;*[contact@revrag.ai](mailto:contact@revrag.ai)** to configure your webhook settings.

## Webhook Headers [#webhook-headers]

Every webhook request includes the following headers:

| Header                | Purpose                                         | Example                     |
| --------------------- | ----------------------------------------------- | --------------------------- |
| `Content-Type`        | Ensures receiver parses payload correctly       | `application/json`          |
| `X-Webhook-Event`     | Identifies the event type                       | `call.ended`                |
| `X-Webhook-ID`        | Unique identifier for deduplication and logging | `evt_01HC3Q0MZQABR3...`     |
| `X-Webhook-Timestamp` | Unix timestamp when the webhook was sent        | `1698064496`                |
| `X-Webhook-Signature` | HMAC signature for verifying authenticity       | `t=1698064496,v1=abc123...` |

## Webhook Payload [#webhook-payload]

The webhook payload contains the same data structure as described in the [Webhook Notifications section](/api-reference/call-trigger-apis#4-webhook-notifications-optional) of the API documentation. For complete payload details and field descriptions, please refer to that section.

## Signature Verification [#signature-verification]

### How It Works [#how-it-works]

We use HMAC-SHA256 to sign every webhook request. The signature is computed over a string consisting of:

```
timestamp + "." + raw_request_body
```

Where:

* `timestamp` is the Unix timestamp (in seconds) from the `X-Webhook-Timestamp` header
* `raw_request_body` is the exact byte sequence of the JSON payload (before parsing)

### Signature Format [#signature-format]

The `X-Webhook-Signature` header contains two components:

```
t=<unix_timestamp>,v1=<hex_encoded_hmac_signature>
```

* `t`: The timestamp when the signature was generated
* `v1`: The HMAC-SHA256 signature in hexadecimal format

### Verification Steps [#verification-steps]

To verify a webhook request is authentic, follow these steps:

1. **Extract the timestamp and signature** from the `X-Webhook-Signature` header
2. **Verify the timestamp** is within an acceptable time window (recommended: ±5 minutes) to prevent replay attacks
3. **Reconstruct the signed string** by concatenating the timestamp, a period, and the raw request body
4. **Compute the HMAC-SHA256** using your shared secret
5. **Compare signatures** using a constant-time comparison function to prevent timing attacks
6. **Check the X-Webhook-ID** (optional but recommended) to prevent duplicate processing

### Implementation Examples [#implementation-examples]

#### Python [#python]

```python
import hmac
import hashlib
import time
from flask import request, abort

WEBHOOK_SECRET = "your_32_byte_secret_key"  # 256-bit secret
TOLERANCE_SECONDS = 300  # 5 minutes

def verify_webhook():
    # Get the raw request body
    raw_body = request.get_data()
    
    # Parse the signature header
    signature_header = request.headers.get('X-Webhook-Signature', '')
    parts = dict(item.split('=') for item in signature_header.split(','))
    
    timestamp_str = parts.get('t')
    received_signature = parts.get('v1')
    
    if not timestamp_str or not received_signature:
        abort(401, "Missing signature components")
    
    # Verify timestamp to prevent replay attacks
    timestamp = int(timestamp_str)
    current_time = int(time.time())
    
    if abs(current_time - timestamp) > TOLERANCE_SECONDS:
        abort(401, "Timestamp outside tolerance window")
    
    # Reconstruct the signed payload
    signed_payload = f"{timestamp_str}.{raw_body.decode('utf-8')}"
    
    # Compute the expected signature
    expected_signature = hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        signed_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    # Constant-time comparison to prevent timing attacks
    if not hmac.compare_digest(expected_signature, received_signature):
        abort(401, "Invalid signature")
    
    # Optional: Check for duplicate webhook IDs
    webhook_id = request.headers.get('X-Webhook-ID')
    # Store and check webhook_id in your database to prevent duplicates
    
    return True
```

#### Node.js (Express) [#nodejs-express]

```javascript
const crypto = require('crypto');
const express = require('express');

const WEBHOOK_SECRET = 'your_32_byte_secret_key';  // 256-bit secret
const TOLERANCE_SECONDS = 300;  // 5 minutes

function verifyWebhook(req, res, next) {
  // Get raw body (ensure you're using express.raw() middleware)
  const rawBody = req.body;
  
  // Parse the signature header
  const signatureHeader = req.headers['x-webhook-signature'] || '';
  const parts = {};
  signatureHeader.split(',').forEach(part => {
    const [key, value] = part.split('=');
    parts[key] = value;
  });
  
  const timestampStr = parts.t;
  const receivedSignature = parts.v1;
  
  if (!timestampStr || !receivedSignature) {
    return res.status(401).send('Missing signature components');
  }
  
  // Verify timestamp to prevent replay attacks
  const timestamp = parseInt(timestampStr, 10);
  const currentTime = Math.floor(Date.now() / 1000);
  
  if (Math.abs(currentTime - timestamp) > TOLERANCE_SECONDS) {
    return res.status(401).send('Timestamp outside tolerance window');
  }
  
  // Reconstruct the signed payload
  const signedPayload = `${timestampStr}.${rawBody.toString('utf-8')}`;
  
  // Compute the expected signature
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(signedPayload)
    .digest('hex');
  
  // Constant-time comparison to prevent timing attacks
  if (!crypto.timingSafeEqual(
    Buffer.from(expectedSignature),
    Buffer.from(receivedSignature)
  )) {
    return res.status(401).send('Invalid signature');
  }
  
  // Optional: Check for duplicate webhook IDs
  const webhookId = req.headers['x-webhook-id'];
  // Store and check webhookId in your database to prevent duplicates
  
  next();
}

// Use with express.raw() to preserve the raw body
app.use('/webhooks', express.raw({ type: 'application/json' }));
app.post('/webhooks', verifyWebhook, (req, res) => {
  // Process the webhook
  const payload = JSON.parse(req.body.toString());
  // ... handle the webhook event
  res.sendStatus(200);
});
```

## Security Best Practices [#security-best-practices]

### 1. Use HTTPS Only [#1-use-https-only]

Always use HTTPS endpoints for webhooks. We reject HTTP URLs to prevent man-in-the-middle attacks.

### 2. Validate the Timestamp [#2-validate-the-timestamp]

Always verify the timestamp is within an acceptable window (recommended: ±5 minutes). This prevents replay attacks where an attacker attempts to resend a captured webhook.

### 3. Use Constant-Time Comparison [#3-use-constant-time-comparison]

Always use constant-time comparison functions (like `hmac.compare_digest` in Python or `crypto.timingSafeEqual` in Node.js) when comparing signatures. This prevents timing attacks.

### 4. Store the Secret Securely [#4-store-the-secret-securely]

* Generate a cryptographically secure random secret (32 bytes / 256 bits)
* Store it encrypted using a key management service (KMS, HashiCorp Vault, etc.)
* Never commit secrets to version control
* Rotate secrets periodically

### 5. Implement Idempotency [#5-implement-idempotency]

Use the `X-Webhook-ID` header to track which webhooks you've already processed. Store this ID in your database and reject duplicate deliveries.

### 6. Verify the Raw Body [#6-verify-the-raw-body]

The signature is computed over the **raw request bytes**, not the parsed JSON. Make sure your verification code uses the exact body bytes received, before any parsing or transformation.

### 7. Return Appropriate Status Codes [#7-return-appropriate-status-codes]

* Return `200` or `204` for successful processing
* Return `401` for signature verification failures
* Return `400` for malformed requests
* Return `500` for internal server errors

We will retry failed webhook deliveries with exponential backoff for status codes in the 5xx range.

## Troubleshooting [#troubleshooting]

### Signature Verification Fails [#signature-verification-fails]

**Common causes:**

* Using parsed JSON instead of raw request body for verification
* Incorrect timestamp extraction or format
* Secret key mismatch
* Using string concatenation instead of proper byte operations
* Not using UTF-8 encoding consistently

**Solution:**

* Ensure you're computing the HMAC over the exact raw bytes received
* Verify your secret key matches what was configured
* Check that you're extracting the timestamp correctly from the header
* Use logging to compare your computed signature with the received signature

### Timestamp Out of Range [#timestamp-out-of-range]

**Common causes:**

* Server clock drift
* Processing delays
* Timezone issues

**Solution:**

* Synchronize your server clock using NTP
* Use Unix timestamps (seconds since epoch) in UTC
* Adjust your tolerance window if needed (but don't exceed 10 minutes)

### Duplicate Webhooks [#duplicate-webhooks]

**Common causes:**

* Network issues causing retries
* Not implementing idempotency checks

**Solution:**

* Track `X-Webhook-ID` values in your database
* Implement idempotent webhook handlers that can safely process the same event multiple times

## Testing Webhooks [#testing-webhooks]

When testing webhook integration, you can use tools like:

* **ngrok** or **localtunnel** to expose your local server to receive webhooks
* **Request logging** to inspect the exact headers and body received
* **Manual signature generation** to test your verification logic

Contact support at &#x2A;*[contact@revrag.ai](mailto:contact@revrag.ai)** for assistance with testing webhooks in the staging environment.

## Support [#support]

If you encounter any issues with webhook setup or signature verification, please contact our support team:

**Email**: [contact@revrag.ai](mailto:contact@revrag.ai)
