Webhooks Documentation

Complete guide to integrating real-time webhook notifications for your chatbot events

Introduction

Webhooks allow your application to receive real-time notifications when events occur in your chatbot system. Instead of continuously polling our API for updates, webhooks push event data to your specified endpoint instantly when something happens.
Webhooks are ideal for:
  • Monitoring chatbot conversations in real-time
  • Syncing conversation data with your CRM or analytics platform
  • Triggering workflows when users request human assistance
  • Tracking knowledge base updates
  • Building custom integrations and automations

Getting Started with Webhooks

Follow these steps to set up your first webhook:
Step 1: Open Your Chatbot
From the main menu, click on Chatbots and open or edit the chatbot you want to configure webhooks for.
Step 2: Navigate to Webhooks
In the chatbot sidebar menu, click Webhooks.
Step 3: Create a Webhook
Click the Create Webhook button. A modal will appear with the following fields:
  • Webhook URL (required): Your HTTPS endpoint that will receive webhook events
  • Select Events (required): Choose which events should trigger this webhook

Note: Webhooks are scoped to the chatbot you are currently editing. Each chatbot manages its own webhooks independently.
Step 4: Save and Copy Your Secret Key
After clicking Create, your webhook secret key will be displayed for 30 seconds only. This is the only time you will see the full secret key.
CRITICAL: Copy and securely store this secret key immediately. You will need it to verify webhook authenticity. If you lose it, you'll need to regenerate a new secret key.

Webhook URL Requirements

Your webhook endpoint must meet these requirements:
HTTPS Only: All webhook URLs must use HTTPS protocol. HTTP endpoints are not supported for security reasons.
Publicly Accessible: Your endpoint must be accessible from the internet. Localhost or internal network URLs won't work in production.
Fast Response: Your endpoint should respond within 30 seconds. Requests that timeout will be marked as failed.
Return 200 OK: Your endpoint should return a 200 OK status code to acknowledge successful receipt of the webhook.
Example Valid URLs:
bash
https://api.example.com/webhooks
https://myapp.example.com/chatbot-events
https://webhook.example.com/notifications

Webhook Authentication

Every webhook request includes an HMAC-SHA256 signature that you must verify to ensure the request came from our system and hasn't been tampered with.
How It Works:
1. We generate an HMAC signature of the webhook payload using your secret key
2. The signature is sent in the X-Webhook-Signature header
3. You compute the same signature on your end using the payload and your secret key
4. Compare the signatures - if they match, the webhook is authentic
Security Benefits:
  • Confirms the webhook came from SolidScale Chatbots
  • Ensures the payload hasn't been modified in transit
  • Prevents replay attacks when combined with timestamp validation

Signature Format

The webhook signature is sent in the X-Webhook-Signature header in the following format:
bash
sha256=3d5a2c8f9e1b4a6d7c8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c

Webhook Request Headers

Every webhook request includes these headers:
X-Webhook-Signature (string, required)
HMAC-SHA256 signature of the request payload. Format: sha256={hash}
Use this to verify the webhook authenticity.
X-Webhook-Event (string, required)
The event type that triggered this webhook.
Examples: conversation.started, message.received, message.sent
X-Webhook-Delivery-Id (string, required)
Unique identifier for this delivery attempt. Format: del_{uuid}
Useful for logging and debugging duplicate deliveries.
X-Webhook-Timestamp (string, required)
Unix timestamp (seconds since epoch) when the webhook was sent.
Use this to reject old webhooks and prevent replay attacks.
User-Agent (string, required)
Identifies the webhook sender. Value: SolidScale-Webhook/1.0
Content-Type (string, required)
Always application/json - all webhook payloads are JSON.
bash
X-Webhook-Signature: sha256=abc123...
X-Webhook-Event: message.received
X-Webhook-Delivery-Id: del_a1b2c3d4e5f6
X-Webhook-Timestamp: 1729180800
User-Agent: SolidScale-Webhook/1.0
Content-Type: application/json

Webhook Payload Structure

All webhook events follow a consistent JSON structure. The data field contains event-specific information that varies by event type.
json
{
  "event": "message.received",
  "eventId": "evt_a1b2c3d4e5f6789",
  "timestamp": "2025-10-21T14:30:00.000Z",
  "chatbotId": 1,
  "chatbotName": "Customer Support Bot",
  "userId": 42,
  "data": {
    // Event-specific data here
    // Structure varies by event type
  }
}

Common Payload Fields

event (string, required)
The type of event that occurred. Possible values: conversation.started, message.received, message.sent, knowledge.added, knowledge.deleted, human.handoff.requested
eventId (string, required)
Unique identifier for this event. Format: evt_{randomString}
Useful for deduplication and logging.
timestamp (string, required)
ISO 8601 timestamp when the event occurred.
Example: 2025-10-21T14:30:00.000Z
chatbotId (integer, required)
The ID of the chatbot associated with this event.
Will be 0 for events not tied to a specific chatbot.
chatbotName (string, nullable)
The name of the chatbot, if applicable.
userId (integer, required)
The ID of the user (chatbot owner) associated with this event.
data (object, required)
Event-specific payload. Structure varies by event type.
See individual event documentation for details.

Response Requirements

Your webhook endpoint should respond appropriately to different types of webhook requests.
For Regular Webhook Events:
Return a 200 OK status code to acknowledge successful receipt.
The response body is optional and will be logged but not processed.
For Webhook Verification (during setup):
When you create a webhook and click "Verify", we send a test request with a challenge token.
Your endpoint should return a 200 OK with the challenge echoed back in the response body:
json
{
  "challenge": "abc123-challenge-token-xyz789"
}

Timeout Handling

Request Timeout: Webhook requests have a 30-second timeout. If your endpoint doesn't respond within this time, the delivery will be marked as failed.
Best Practices:
  • Respond with 200 OK immediately upon receiving the webhook
  • Process the webhook payload asynchronously after responding
  • Don't perform long-running operations synchronously
  • Use a queue system for complex processing

Example Pattern:
javascript
app.post('/webhooks', async (req, res) => {
  // 1. Verify signature first
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid signature');
  }
  
  // 2. Respond immediately
  res.status(200).send('OK');
  
  // 3. Process asynchronously
  await queueWebhookForProcessing(req.body);
});

Webhook Event Types

Our webhook system supports six event types that cover the entire lifecycle of chatbot interactions and knowledge management. Subscribe to the events relevant to your use case.

Event: conversation.started

Triggered when: A new conversation begins with a chatbot.
This event fires when a user sends their first message in a new session. Use this to:
  • Track new conversation creation
  • Initialize user sessions in your CRM
  • Start conversation analytics
  • Set up real-time monitoring dashboards

Event Name: conversation.started

conversation.started - Payload Example

Here's what the webhook payload looks like when a conversation starts:
json
{
  "event": "conversation.started",
  "eventId": "evt_a1b2c3d4e5f6789",
  "timestamp": "2025-10-21T14:30:00.000Z",
  "chatbotId": 1,
  "chatbotName": "Customer Support Bot",
  "userId": 42,
  "data": {
    "message": "Hi, I need help with my order",
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "chatbotId": 1,
    "startedAt": "2025-10-21T14:30:00.000Z"
  }
}

conversation.started - Data Fields

data.message (string, required)
The first message sent by the user that initiated the conversation.
data.sessionId (string, required)
Unique session identifier (UUID format) for this conversation.
Use this to track all messages in the same conversation.
data.chatbotId (integer, required)
The ID of the chatbot involved in this conversation.
data.startedAt (string, required)
ISO 8601 timestamp when the conversation started.
Matches the top-level timestamp field.

Event: message.received

Triggered when: A user sends a message in an existing conversation.
This event fires for every user message after the first one in a session. Use this to:
  • Monitor ongoing conversations
  • Analyze user questions and sentiment
  • Track conversation flow and user behavior
  • Build conversation transcripts
  • Identify common user queries

Event Name: message.received
Note: The first message in a new session triggers conversation.started instead of message.received.

message.received - Payload Example

Example webhook payload when a user sends a message:
json
{
  "event": "message.received",
  "eventId": "evt_b2c3d4e5f6a7890",
  "timestamp": "2025-10-21T14:31:15.000Z",
  "chatbotId": 1,
  "chatbotName": "Customer Support Bot",
  "userId": 42,
  "data": {
    "message": "What's the status of order #12345?",
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "chatbotId": 1,
    "timestamp": "2025-10-21T14:31:15.000Z"
  }
}

message.received - Data Fields

data.message (string, required)
The text message sent by the user.
data.sessionId (string, required)
Session identifier (UUID) linking this message to its conversation.
Use this to correlate messages in the same conversation.
data.chatbotId (integer, required)
The ID of the chatbot receiving the message.
data.timestamp (string, required)
ISO 8601 timestamp when the user sent the message.

Event: message.sent

Triggered when: The chatbot sends a response to the user.
This event fires immediately after the chatbot generates and sends a reply. Use this to:
  • Monitor chatbot response quality
  • Track response times and performance
  • Build complete conversation logs
  • Analyze bot effectiveness
  • Measure customer satisfaction

Event Name: message.sent

message.sent - Payload Example

Example webhook payload when the chatbot responds:
json
{
  "event": "message.sent",
  "eventId": "evt_c3d4e5f6a7b8901",
  "timestamp": "2025-10-21T14:31:18.000Z",
  "chatbotId": 1,
  "chatbotName": "Customer Support Bot",
  "userId": 42,
  "data": {
    "response": "Let me check the status of order #12345 for you. Your order was shipped on October 20th and should arrive by October 23rd. The tracking number is 1Z999AA1234567890.",
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "chatbotId": 1,
    "responseTimeMs": 1247,
    "timestamp": "2025-10-21T14:31:18.000Z"
  }
}

message.sent - Data Fields

data.response (string, required)
The complete text response generated and sent by the chatbot.
data.sessionId (string, required)
Session identifier (UUID) for this conversation.
data.chatbotId (integer, required)
The ID of the chatbot that generated this response.
data.responseTimeMs (integer, required)
Time taken to generate the response in milliseconds.
Measured from when the user message was received to when the bot response was ready.
data.timestamp (string, required)
ISO 8601 timestamp when the response was sent.

Complete Conversation Flow

Here's how the three message events work together in a typical conversation:
Step 1: User starts a new chat → conversation.started event
Step 2: User asks a follow-up question → message.received event
Step 3: Bot responds to the question → message.sent event
Step 4: User sends another message → message.received event
Step 5: Bot responds again → message.sent event
Each event includes the same sessionId, allowing you to reconstruct the entire conversation by grouping events by session.

Event: knowledge.added

Triggered when: A new knowledge source is added to the system.
This event fires when you upload a document or add a URL as a knowledge source. Use this to:
  • Track knowledge base updates
  • Sync knowledge sources with external systems
  • Trigger retraining workflows
  • Monitor content additions
  • Maintain knowledge source inventory

Event Name: knowledge.added
Note: This event is not tied to a specific chatbot initially. The chatbotId will be 0 or null until the knowledge source is associated with a chatbot.

knowledge.added - Payload Example

Example webhook payload when a knowledge source is added:
json
{
  "event": "knowledge.added",
  "eventId": "evt_d4e5f6a7b8c9012",
  "timestamp": "2025-10-21T15:00:00.000Z",
  "chatbotId": 0,
  "chatbotName": null,
  "userId": 42,
  "data": {
    "message": "A new document has been uploaded as a knowledge source.",
    "knowledgeSource": {
      "id": 123,
      "name": "Product Documentation Q4 2024",
      "type": "document",
      "sourceUrl": null,
      "fileName": "product-docs-q4-2024.pdf",
      "fileSize": 2458624,
      "fileType": "application/pdf",
      "status": "active",
      "lastProcessedAt": "2025-10-21T15:00:05.000Z",
      "createdAt": "2025-10-21T15:00:00.000Z",
      "updatedAt": "2025-10-21T15:00:00.000Z",
      "usedByChatbotsCount": 0,
      "uploadedBy": 42
    }
  }
}

knowledge.added - Data Fields

data.message (string, required)
A descriptive message about the event.
data.knowledgeSource (object, required)
Complete details about the knowledge source that was added.
data.knowledgeSource.id (integer, required)
Unique identifier for this knowledge source.
data.knowledgeSource.name (string, required)
User-provided name for the knowledge source.
data.knowledgeSource.type (string, required)
Type of knowledge source. Values: document or url
data.knowledgeSource.sourceUrl (string, nullable)
The URL if type is url, otherwise null.
data.knowledgeSource.fileName (string, nullable)
Original filename if type is document, otherwise null.
data.knowledgeSource.fileSize (integer, nullable)
File size in bytes if type is document, otherwise null.
data.knowledgeSource.fileType (string, nullable)
MIME type of the file (e.g., application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document).
Null for URL sources.
data.knowledgeSource.status (string, required)
Current status. Values: active, inactive, processing
data.knowledgeSource.lastProcessedAt (string, nullable)
ISO 8601 timestamp of last processing, if applicable.
data.knowledgeSource.createdAt (string, required)
ISO 8601 timestamp when the source was created.
data.knowledgeSource.updatedAt (string, required)
ISO 8601 timestamp of last update.
data.knowledgeSource.usedByChatbotsCount (integer, required)
Number of chatbots currently using this knowledge source.
data.knowledgeSource.uploadedBy (integer, required)
User ID of the person who added this knowledge source.

knowledge.added - URL Source Example

When a URL is added as a knowledge source, the payload structure is slightly different:
json
{
  "event": "knowledge.added",
  "eventId": "evt_e5f6a7b8c9d0123",
  "timestamp": "2025-10-21T15:15:00.000Z",
  "chatbotId": 0,
  "chatbotName": null,
  "userId": 42,
  "data": {
    "message": "A new knowledge source has been added.",
    "knowledgeSource": {
      "id": 124,
      "name": "FAQ Page",
      "type": "url",
      "sourceUrl": "https://example.com/faq",
      "fileName": null,
      "fileSize": null,
      "fileType": null,
      "createdAt": "2025-10-21T15:15:00.000Z",
      "createdBy": 42
    }
  }
}

Event: knowledge.deleted

Triggered when: A knowledge source is deleted from the system.
This event fires when you remove a knowledge source. Use this to:
  • Track knowledge base changes
  • Sync deletions with external systems
  • Maintain audit logs
  • Update content inventories
  • Trigger chatbot retraining if needed

Event Name: knowledge.deleted

knowledge.deleted - Payload Example

Example webhook payload when a knowledge source is deleted:
json
{
  "event": "knowledge.deleted",
  "eventId": "evt_f6a7b8c9d0e1234",
  "timestamp": "2025-10-21T16:00:00.000Z",
  "chatbotId": 0,
  "chatbotName": null,
  "userId": 42,
  "data": {
    "message": "A knowledge source has been deleted.",
    "knowledgeSource": {
      "id": 123,
      "deletedBy": 42,
      "deletedAt": "2025-10-21T16:00:00.000Z"
    }
  }
}

knowledge.deleted - Data Fields

data.message (string, required)
A descriptive message about the deletion event.
data.knowledgeSource (object, required)
Minimal information about the deleted knowledge source.
data.knowledgeSource.id (integer, required)
The ID of the knowledge source that was deleted.
data.knowledgeSource.deletedBy (integer, required)
User ID of the person who deleted the knowledge source.
data.knowledgeSource.deletedAt (string, required)
ISO 8601 timestamp when the deletion occurred.

Event: human.handoff.requested

Triggered when: A user requests to speak with a human agent or the system detects a need for human intervention.
This event fires when:
  • User explicitly asks to talk to a human (e.g., "I want to speak to a person")
  • The system detects phrases indicating need for human help
  • Previous conversation context suggests human assistance is needed

Use this to:
  • Route conversations to human agents
  • Alert support teams of pending requests
  • Track AI-to-human handoff rates
  • Integrate with helpdesk systems
  • Monitor escalation patterns

Event Name: human.handoff.requested

human.handoff.requested - Payload Example

Example webhook payload when human assistance is requested:
json
{
  "event": "human.handoff.requested",
  "eventId": "evt_g7b8c9d0e1f2345",
  "timestamp": "2025-10-21T14:35:00.000Z",
  "chatbotId": 1,
  "chatbotName": "Customer Support Bot",
  "userId": 42,
  "data": {
    "interactionId": 5678,
    "message": "I need to speak with someone about canceling my subscription",
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "chatbotId": 1,
    "userId": 42,
    "timestamp": "2025-10-21T14:35:00.000Z",
    "conversationContext": [
      {
        "userMessage": "Hi, I need help with my subscription",
        "botResponse": "I'd be happy to help with your subscription. What would you like to know?",
        "timestamp": "2025-10-21T14:30:00.000Z"
      },
      {
        "userMessage": "How do I cancel it?",
        "botResponse": "You can cancel your subscription by going to Settings > Subscription > Cancel. Would you like me to guide you through it?",
        "timestamp": "2025-10-21T14:32:00.000Z"
      },
      {
        "userMessage": "I tried that but I'm having issues",
        "botResponse": "I understand you're having difficulties. Let me help you troubleshoot...",
        "timestamp": "2025-10-21T14:33:00.000Z"
      },
      {
        "userMessage": "This isn't working. Can I talk to a real person?",
        "botResponse": null,
        "timestamp": "2025-10-21T14:34:30.000Z"
      }
    ]
  }
}

human.handoff.requested - Data Fields

data.interactionId (integer, required)
Unique identifier for this interaction record.
Use this to update the interaction when a human responds.
data.message (string, required)
The user message that triggered the handoff request.
data.sessionId (string, required)
Session identifier (UUID) for this conversation.
data.chatbotId (integer, required)
The ID of the chatbot involved in this handoff.
data.userId (integer, required)
The chatbot owner's user ID.
data.timestamp (string, required)
ISO 8601 timestamp when the handoff was requested.
data.conversationContext (array, required)
Array of recent messages (up to last 5) providing conversation context.
Helps human agents understand the situation quickly.
conversationContext[].userMessage (string, required)
The message sent by the user.
conversationContext[].botResponse (string, nullable)
The bot's response. May be null for the triggering message.
conversationContext[].timestamp (string, required)
ISO 8601 timestamp of this exchange.

Responding to Human Handoff Requests

When you receive a human.handoff.requested event, the chatbot is waiting for a human response. The system polls for up to 60 seconds (30 attempts at 2-second intervals).
To provide a human response:
1. Extract the interactionId from the webhook payload
2. Use your backend system to update the interaction record
3. Set the BotResponse field with the human agent's message
4. Set ResponseStatus to "answered"
5. Keep ConversationMode as "human" to maintain human mode
Important: If no response is provided within 60 seconds, the user receives a timeout message: "Sorry our human representative is not available right now, please check back later."

Verifying Webhook Signatures

Always verify webhook signatures to ensure requests are authentic and haven't been tampered with. Here's how to implement signature verification in different languages.

Verification Example - Node.js

Verify HMAC-SHA256 signatures in Node.js using the crypto module:
javascript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secretKey) {
  // Generate expected signature
  const expectedSignature = 'sha256=' + 
    crypto
      .createHmac('sha256', secretKey)
      .update(JSON.stringify(payload))
      .digest('hex');
  
// Use timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Example usage in Express
app.post('/webhooks', express.json(), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const secretKey = process.env.WEBHOOK_SECRET_KEY;
  
  if (!verifyWebhookSignature(req.body, signature, secretKey)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Signature is valid - respond immediately
  res.status(200).send('OK');
  
  // Process webhook asynchronously
  processWebhook(req.body);
});

Verification Example - Python

Verify HMAC-SHA256 signatures in Python using the hmac module:
python
import hmac
import hashlib
import json

def verify_webhook_signature(payload, signature, secret_key):
    """Verify the webhook signature"""
    
    # Generate expected signature
    payload_json = json.dumps(payload, separators=(',', ':'))
    expected_signature = 'sha256=' + hmac.new(
        secret_key.encode('utf-8'),
        payload_json.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    # Use constant-time comparison
    return hmac.compare_digest(signature, expected_signature)

# Example usage in Flask
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Webhook-Signature')
    secret_key = os.getenv('WEBHOOK_SECRET_KEY')
    
    if not verify_webhook_signature(request.json, signature, secret_key):
        return jsonify({'error': 'Invalid signature'}), 401
    
    # Signature is valid
    # Process webhook asynchronously here
    
    return 'OK', 200

Verification Example - C# / .NET

Verify HMAC-SHA256 signatures in C# / .NET:
csharp
using System;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

public class WebhookVerifier
{
    public static bool VerifyWebhookSignature(
        object payload, 
        string signature, 
        string secretKey)
    {
        // Serialize payload to JSON
        var payloadJson = JsonSerializer.Serialize(payload);
        var payloadBytes = Encoding.UTF8.GetBytes(payloadJson);
        var keyBytes = Encoding.UTF8.GetBytes(secretKey);
        
        // Generate expected signature
        using var hmac = new HMACSHA256(keyBytes);
        var hashBytes = hmac.ComputeHash(payloadBytes);
        var expectedSignature = "sha256=" + 
            BitConverter.ToString(hashBytes)
                .Replace("-", "")
                .ToLower();
        
        // Constant-time comparison
        return CryptographicEquals(signature, expectedSignature);
    }
    
    private static bool CryptographicEquals(string a, string b)
    {
        if (a == null || b == null || a.Length != b.Length)
            return false;
        
        var result = 0;
        for (var i = 0; i < a.Length; i++)
        {
            result |= a[i] ^ b[i];
        }
        
        return result == 0;
    }
}

// Example usage in ASP.NET Core
[ApiController]
[Route("api/webhooks")]
public class WebhookController : ControllerBase
{
    [HttpPost]
    public IActionResult HandleWebhook([FromBody] JsonElement payload)
    {
        var signature = Request.Headers["X-Webhook-Signature"];
        var secretKey = Environment.GetEnvironmentVariable("WEBHOOK_SECRET_KEY");
        
        if (!WebhookVerifier.VerifyWebhookSignature(payload, signature, secretKey))
        {
            return Unauthorized(new { error = "Invalid signature" });
        }
        
        // Process webhook asynchronously
        _ = ProcessWebhookAsync(payload);
        
        return Ok("OK");
    }
}

Verification Example - PHP

Verify HMAC-SHA256 signatures in PHP:
php
<?php

function verifyWebhookSignature($payload, $signature, $secretKey) {
    // Generate expected signature
    $payloadJson = json_encode($payload);
    $expectedSignature = 'sha256=' . hash_hmac('sha256', $payloadJson, $secretKey);
    
    // Use timing-safe comparison
    return hash_equals($signature, $expectedSignature);
}

// Example usage
$payload = json_decode(file_get_contents('php://input'), true);
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];
$secretKey = getenv('WEBHOOK_SECRET_KEY');

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

// Signature is valid - respond immediately
http_response_code(200);
echo 'OK';

// Process webhook asynchronously
processWebhook($payload);
?>

Security Best Practices

Always Verify Signatures
Never trust webhook data without verifying the HMAC signature. This prevents attackers from sending fake webhooks to your endpoint.
Use HTTPS Only
Always use HTTPS for your webhook endpoints. Never use plain HTTP, as it exposes your secret key and webhook data to interception.
Validate Timestamps
Check the X-Webhook-Timestamp header and reject webhooks older than a few minutes (e.g., 5 minutes). This prevents replay attacks.
Store Secrets Securely
Never hardcode your webhook secret in your application code. Use environment variables, secret managers (like AWS Secrets Manager, Azure Key Vault), or secure configuration systems.
Implement Idempotency
Use the eventId or X-Webhook-Delivery-Id to detect and ignore duplicate webhook deliveries. Store processed IDs in a database or cache.
Use Constant-Time Comparison
Always use timing-safe comparison functions (like crypto.timingSafeEqual in Node.js) to compare signatures. Regular string comparison is vulnerable to timing attacks.
Respond Quickly
Respond with 200 OK immediately after verifying the signature. Process the webhook payload asynchronously to avoid timeouts.
Implement Rate Limiting
Protect your webhook endpoint from abuse by implementing rate limiting based on IP address or other factors.
Log Everything
Log all webhook deliveries (successful and failed) with the delivery ID, event type, and timestamp for debugging and audit purposes.
Monitor for Anomalies
Set up alerts for unusual patterns like sudden spikes in failed verifications, which could indicate an attack.

Timestamp Validation Example

Implement timestamp validation to prevent replay attacks:
javascript
function validateWebhookTimestamp(timestampHeader) {
  const webhookTimestamp = parseInt(timestampHeader);
  const currentTimestamp = Math.floor(Date.now() / 1000);
  
  // Reject webhooks older than 5 minutes
  const MAX_AGE_SECONDS = 5 * 60;
  const age = currentTimestamp - webhookTimestamp;
  
  if (age > MAX_AGE_SECONDS) {
    return false; // Webhook too old
  }
  
  if (age < -60) {
    return false; // Webhook timestamp is in the future (clock skew)
  }
  
  return true;
}

// Example usage
app.post('/webhooks', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const timestamp = req.headers['x-webhook-timestamp'];
  
  // Validate timestamp first
  if (!validateWebhookTimestamp(timestamp)) {
    return res.status(401).json({ error: 'Webhook timestamp invalid or too old' });
  }
  
  // Then verify signature
  if (!verifyWebhookSignature(req.body, signature, secretKey)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  res.status(200).send('OK');
  processWebhook(req.body);
});

Implementing Idempotency

Prevent duplicate processing using event IDs:
javascript
const processedEvents = new Set(); // Use Redis in production

async function processWebhook(payload) {
  const eventId = payload.eventId;
  
  // Check if we've already processed this event
  if (processedEvents.has(eventId)) {
    console.log(`Duplicate event ${eventId} ignored`);
    return;
  }
  
  // Process the webhook
  try {
    await handleWebhookEvent(payload);
    
    // Mark as processed (with TTL in production)
    processedEvents.add(eventId);
    
    // In production, use Redis with expiration:
    // await redis.setex(`processed:${eventId}`, 86400, '1'); // 24h TTL
    
  } catch (error) {
    console.error(`Error processing event ${eventId}:`, error);
    // Don't mark as processed if there was an error
    throw error;
  }
}

// Example with Redis
const Redis = require('ioredis');
const redis = new Redis();

async function processWebhookWithRedis(payload) {
  const eventId = payload.eventId;
  const lockKey = `webhook:processing:${eventId}`;
  
  // Try to acquire lock (prevents concurrent processing)
  const acquired = await redis.set(lockKey, '1', 'EX', 3600, 'NX');
  
  if (!acquired) {
    console.log(`Event ${eventId} already being processed`);
    return;
  }
  
  try {
    await handleWebhookEvent(payload);
  } finally {
    // Release lock after processing
    await redis.del(lockKey);
  }
}

Testing Your Webhooks

Step 1: Create a Webhook
In the Integrations section, create a webhook with your test endpoint URL.
Step 2: Verify Your Endpoint
Click the "Verify" button. We'll send a test request with a challenge token. Your endpoint should return 200 OK with the challenge in the response body.
Step 3: Test Event Delivery
Click the "Test" button to send a webhook.test event to your endpoint. Check your logs to confirm receipt.
Step 4: Trigger Real Events
  • Start a conversation with your chatbot to trigger conversation.started
  • Send messages to trigger message.received and message.sent
  • Upload a document to trigger knowledge.added
  • Delete a knowledge source to trigger knowledge.deleted

Step 5: Monitor Delivery Logs
View webhook delivery logs in the Integrations dashboard to see:
  • Delivery status (success/failed)
  • Response status codes
  • Response times
  • Error messages for failed deliveries

Test Event Payload

When you click "Test" in the webhook interface, you'll receive a special test event:
json
{
  "event": "webhook.test",
  "eventId": "evt_test123abc",
  "timestamp": "2025-10-21T10:30:00.000Z",
  "chatbotId": 1,
  "chatbotName": "Test Chatbot",
  "userId": 42,
  "data": {
    "message": "This is a test webhook event",
    "testTimestamp": "2025-10-21T10:30:00.000Z"
  }
}