Webhook Security
Learn how to securely receive and verify webhook notifications from RevRag.ai
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 contact@revrag.ai to configure your webhook settings.
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
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_bodyWhere:
timestampis the Unix timestamp (in seconds) from theX-Webhook-Timestampheaderraw_request_bodyis 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 generatedv1: The HMAC-SHA256 signature in hexadecimal format
Verification Steps
To verify a webhook request is authentic, follow these steps:
- Extract the timestamp and signature from the
X-Webhook-Signatureheader - Verify the timestamp is within an acceptable time window (recommended: ±5 minutes) to prevent replay attacks
- Reconstruct the signed string by concatenating the timestamp, a period, and the raw request body
- Compute the HMAC-SHA256 using your shared secret
- Compare signatures using a constant-time comparison function to prevent timing attacks
- 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 TrueNode.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
200or204for successful processing - Return
401for signature verification failures - Return
400for malformed requests - Return
500for 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-IDvalues 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 contact@revrag.ai 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: contact@revrag.ai