Skip to main content

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

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

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 [email protected] to configure your webhook settings.

Webhook Headers

Every webhook request includes the following headers:
HeaderPurposeExample
Content-TypeEnsures receiver parses payload correctlyapplication/json
X-Webhook-EventIdentifies the event typecall.ended
X-Webhook-IDUnique identifier for deduplication and loggingevt_01HC3Q0MZQABR3...
X-Webhook-TimestampUnix timestamp when the webhook was sent1698064496
X-Webhook-SignatureHMAC signature for verifying authenticityt=1698064496,v1=abc123...

Webhook Payload

The webhook payload contains the same data structure as described in the Webhook Notifications section of the API documentation. For complete payload details and field descriptions, please refer to that section.

Signature Verification

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

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

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

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)

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

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

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

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

  • 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

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

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

  • 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

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

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

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

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 [email protected] for assistance with testing webhooks in the staging environment.

Support

If you encounter any issues with webhook setup or signature verification, please contact our support team: Email: [email protected]