security

Webhook Security#

Protect your webhook endpoints from unauthorized access using HMAC signature verification. This ensures webhook requests actually come from AlonChat.

Why Security Matters#

Without verification:

  • ❌ Anyone can send fake webhook requests to your endpoint
  • ❌ Attackers can spam your system with bogus events
  • ❌ Malicious actors can trigger unwanted actions

With HMAC verification:

  • ✅ Only authentic AlonChat requests are accepted
  • ✅ Tampered payloads are rejected
  • ✅ Protects against replay attacks

HMAC Signature#

AlonChat uses HMAC SHA-1 to sign webhook payloads.

How It Works#

  1. AlonChat generates signature:

    • Takes raw JSON payload
    • Signs it with your webhook's secret key
    • Includes signature in x-alonchat-signature header
  2. Your endpoint verifies:

    • Receives raw payload
    • Computes expected signature using same secret key
    • Compares with received signature
    • Accepts if match, rejects if mismatch

Signature Format#

Code
x-alonchat-signature: a3f5c8d9e2b1f4a7c6d8e9f0a1b2c3d4e5f6a7b8

Components:

  • Algorithm: HMAC SHA-1
  • Encoding: Hexadecimal
  • Length: 40 characters

Secret Key#

When you create a webhook, AlonChat generates a 64-character secret key:

Code
a3f5c8d9e2b1f4a7c6d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0

IMPORTANT:

  • 🔐 Shown only once during webhook creation
  • 🔐 Store securely (environment variables, secrets manager)
  • 🔐 Never commit to version control
  • 🔐 Treat like a password

If lost: Delete webhook and create new one (generates new secret).


Verification Implementation#

Node.js / Next.js#

typescript
// app/api/webhooks/alonchat/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';

// Disable body parsing to get raw body
export const config = {
  api: {
    bodyParser: false
  }
};

export async function POST(req: NextRequest) {
  try {
    // 1. Get raw body
    const rawBody = await req.text();

    // 2. Get signature from header
    const receivedSignature = req.headers.get('x-alonchat-signature');

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

    // 3. Compute expected signature
    const expectedSignature = crypto
      .createHmac('sha1', process.env.ALONCHAT_WEBHOOK_SECRET!)
      .update(rawBody)
      .digest('hex');

    // 4. Compare signatures (timing-safe)
    const isValid = crypto.timingSafeEqual(
      Buffer.from(receivedSignature),
      Buffer.from(expectedSignature)
    );

    if (!isValid) {
      return NextResponse.json(
        { error: 'Invalid signature' },
        { status: 401 }
      );
    }

    // 5. Parse and process event
    const event = JSON.parse(rawBody);

    if (event.eventType === 'leads.submit') {
      await saveToCRM(event.payload);
    }

    return NextResponse.json({ success: true });

  } catch (error) {
    console.error('Webhook error:', error);
    return NextResponse.json(
      { error: 'Internal error' },
      { status: 500 }
    );
  }
}

Option 2: Pages Router#

typescript
// pages/api/webhooks/alonchat.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';
import getRawBody from 'raw-body';

export const config = {
  api: {
    bodyParser: false
  }
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  try {
    // 1. Get raw body
    const rawBody = await getRawBody(req);

    // 2. Get signature
    const receivedSignature = req.headers['x-alonchat-signature'] as string;

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

    // 3. Compute expected signature
    const expectedSignature = crypto
      .createHmac('sha1', process.env.ALONCHAT_WEBHOOK_SECRET!)
      .update(rawBody)
      .digest('hex');

    // 4. Timing-safe comparison
    const isValid = crypto.timingSafeEqual(
      Buffer.from(receivedSignature),
      Buffer.from(expectedSignature)
    );

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

    // 5. Process event
    const event = JSON.parse(rawBody.toString());

    if (event.eventType === 'leads.submit') {
      await saveToCRM(event.payload);
    }

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

  } catch (error) {
    console.error('Webhook error:', error);
    return res.status(500).json({ error: 'Internal error' });
  }
}

Install dependencies:

bash
npm install raw-body
npm install -D @types/node

Python / Flask#

python
from flask import Flask, request, jsonify
import hmac
import hashlib
import os

app = Flask(__name__)

@app.route('/webhooks/alonchat', methods=['POST'])
def alonchat_webhook():
    # 1. Get raw body
    raw_body = request.get_data()

    # 2. Get signature
    received_signature = request.headers.get('x-alonchat-signature')

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

    # 3. Compute expected signature
    secret = os.environ.get('ALONCHAT_WEBHOOK_SECRET').encode()
    expected_signature = hmac.new(
        secret,
        raw_body,
        hashlib.sha1
    ).hexdigest()

    # 4. Timing-safe comparison
    is_valid = hmac.compare_digest(received_signature, expected_signature)

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

    # 5. Process event
    event = request.get_json()

    if event['eventType'] == 'leads.submit':
        save_to_crm(event['payload'])

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

PHP#

php
<?php
// webhooks/alonchat.php

// 1. Get raw body
$rawBody = file_get_contents('php://input');

// 2. Get signature
$receivedSignature = $_SERVER['HTTP_X_ALONCHAT_SIGNATURE'] ?? '';

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

// 3. Compute expected signature
$secret = getenv('ALONCHAT_WEBHOOK_SECRET');
$expectedSignature = hash_hmac('sha1', $rawBody, $secret);

// 4. Timing-safe comparison
$isValid = hash_equals($expectedSignature, $receivedSignature);

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

// 5. Process event
$event = json_decode($rawBody, true);

if ($event['eventType'] === 'leads.submit') {
    save_to_crm($event['payload']);
}

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

Ruby / Rails#

ruby
# config/routes.rb
post '/webhooks/alonchat', to: 'webhooks#alonchat'

# app/controllers/webhooks_controller.rb
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def alonchat
    # 1. Get raw body
    raw_body = request.raw_post

    # 2. Get signature
    received_signature = request.headers['x-alonchat-signature']

    if received_signature.blank?
      render json: { error: 'Missing signature' }, status: :unauthorized
      return
    end

    # 3. Compute expected signature
    secret = ENV['ALONCHAT_WEBHOOK_SECRET']
    expected_signature = OpenSSL::HMAC.hexdigest(
      'SHA1',
      secret,
      raw_body
    )

    # 4. Timing-safe comparison
    is_valid = ActiveSupport::SecurityUtils.secure_compare(
      expected_signature,
      received_signature
    )

    unless is_valid
      render json: { error: 'Invalid signature' }, status: :unauthorized
      return
    end

    # 5. Process event
    event = JSON.parse(raw_body)

    if event['eventType'] == 'leads.submit'
      save_to_crm(event['payload'])
    end

    render json: { success: true }, status: :ok
  end
end

Security Best Practices#

1. Always Use Raw Body#

❌ WRONG:

javascript
// Don't use parsed body!
const body = req.body;
const signature = crypto.createHmac('sha1', secret)
  .update(JSON.stringify(body))  // ❌ Won't match!
  .digest('hex');

✅ CORRECT:

javascript
// Use raw body before parsing
const rawBody = await req.text();
const signature = crypto.createHmac('sha1', secret)
  .update(rawBody)  // ✅ Matches!
  .digest('hex');

Why? JSON.stringify may format differently than AlonChat's original payload.


2. Use Timing-Safe Comparison#

❌ WRONG:

javascript
if (receivedSignature === expectedSignature) {
  // ❌ Vulnerable to timing attacks
}

✅ CORRECT:

javascript
const isValid = crypto.timingSafeEqual(
  Buffer.from(receivedSignature),
  Buffer.from(expectedSignature)
);

Why? === reveals information through response time. Timing-safe comparison prevents this.


3. Store Secret Securely#

❌ WRONG:

javascript
// ❌ Hardcoded secret
const secret = 'a3f5c8d9e2b1f4a7...';

✅ CORRECT:

javascript
// ✅ Environment variable
const secret = process.env.ALONCHAT_WEBHOOK_SECRET;

Best practices:

  • Use environment variables
  • Use secrets manager (AWS Secrets Manager, Vercel Environment Variables)
  • Never commit secrets to git
  • Rotate secrets regularly

4. Validate Payload Structure#

After signature verification, validate payload:

typescript
// Verify required fields exist
if (!event.eventType || !event.chatbotId || !event.payload) {
  return res.status(400).json({ error: 'Invalid payload structure' });
}

// Verify event type is expected
const validEvents = ['leads.submit', 'message.sent'];
if (!validEvents.includes(event.eventType)) {
  return res.status(400).json({ error: 'Unknown event type' });
}

// Validate payload fields
if (event.eventType === 'leads.submit') {
  if (!event.payload.customerEmail) {
    return res.status(400).json({ error: 'Missing customerEmail' });
  }
}

5. Implement Idempotency#

Handle duplicate deliveries gracefully:

typescript
const processedEvents = new Map<string, boolean>();

// Use unique identifier (messageId, conversationId + timestamp)
const eventId = event.payload.messageId ||
  `${event.payload.conversationId}-${event.timestamp}`;

// Check if already processed
if (processedEvents.has(eventId)) {
  console.log(`Duplicate event ${eventId}, skipping`);
  return res.status(200).json({ success: true, duplicate: true });
}

// Process event
await handleEvent(event);

// Mark as processed
processedEvents.set(eventId, true);

// Clean up old entries (prevent memory leak)
if (processedEvents.size > 1000) {
  const oldestKey = processedEvents.keys().next().value;
  processedEvents.delete(oldestKey);
}

Better: Use database or Redis for persistent idempotency checks.


6. Rate Limiting#

Protect your endpoint from abuse:

typescript
import rateLimit from 'express-rate-limit';

const webhookLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100, // Max 100 requests per minute
  message: 'Too many webhook requests'
});

app.post('/webhooks/alonchat', webhookLimiter, webhookHandler);

7. Logging & Monitoring#

Log all webhook activity for debugging:

typescript
// Log incoming request
console.log('Webhook received:', {
  eventType: event.eventType,
  timestamp: event.timestamp,
  chatbotId: event.chatbotId
});

// Log verification result
if (!isValid) {
  console.error('Invalid signature:', {
    received: receivedSignature,
    expected: expectedSignature,
    body: rawBody.slice(0, 100) // First 100 chars
  });
}

// Log processing result
try {
  await handleEvent(event);
  console.log('Event processed successfully:', eventId);
} catch (error) {
  console.error('Event processing failed:', error);
}

Testing Signature Verification#

Manual Test#

  1. Get your webhook secret key
  2. Create test payload:
    json
    {"eventType":"leads.submit","chatbotId":"test","payload":{},"timestamp":"2025-11-26T10:30:00Z"}
    
  3. Generate signature (Node.js):
    javascript
    const crypto = require('crypto');
    const payload = '{"eventType":"leads.submit","chatbotId":"test","payload":{},"timestamp":"2025-11-26T10:30:00Z"}';
    const secret = 'your-secret-key';
    const signature = crypto.createHmac('sha1', secret).update(payload).digest('hex');
    console.log(signature);
    
  4. Send request:
    bash
    curl -X POST https://your-endpoint.com/webhooks/alonchat \
      -H "Content-Type: application/json" \
      -H "x-alonchat-signature: <generated-signature>" \
      -d '{"eventType":"leads.submit","chatbotId":"test","payload":{},"timestamp":"2025-11-26T10:30:00Z"}'
    

Unit Test Example#

typescript
import { verifyWebhookSignature } from './webhook-utils';

describe('Webhook Signature Verification', () => {
  it('should verify valid signature', () => {
    const payload = '{"eventType":"test"}';
    const secret = 'test-secret';
    const signature = crypto
      .createHmac('sha1', secret)
      .update(payload)
      .digest('hex');

    const isValid = verifyWebhookSignature(payload, signature, secret);
    expect(isValid).toBe(true);
  });

  it('should reject invalid signature', () => {
    const payload = '{"eventType":"test"}';
    const secret = 'test-secret';
    const signature = 'invalid-signature';

    const isValid = verifyWebhookSignature(payload, signature, secret);
    expect(isValid).toBe(false);
  });
});

Troubleshooting#

"Invalid signature" error#

Common causes:

  1. Using parsed body instead of raw body

    • Solution: Get raw request body before parsing JSON
  2. Wrong secret key

    • Solution: Verify environment variable is correct
    • Check for whitespace or newlines in secret
  3. Body transformation

    • Solution: Ensure no middleware is modifying the body
    • Disable body parser for webhook route
  4. Character encoding issues

    • Solution: Use UTF-8 encoding for body and signature

Debugging:

typescript
console.log('Received signature:', receivedSignature);
console.log('Expected signature:', expectedSignature);
console.log('Raw body length:', rawBody.length);
console.log('Secret key length:', secret.length);

Webhook logs show "401 Unauthorized"#

Check AlonChat webhook logs:

  1. Go to Agent Settings → Webhooks
  2. Click webhook → View Logs
  3. Look for response details

If signature is missing:

  • Webhook configuration might be outdated
  • Recreate webhook with new secret key

If signature is invalid:

  • Verify your implementation matches examples above
  • Test with AlonChat "Test" button first

Next Steps#

security | AlonChat Docs