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#
-
AlonChat generates signature:
- Takes raw JSON payload
- Signs it with your webhook's secret key
- Includes signature in
x-alonchat-signatureheader
-
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#
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:
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#
Option 1: App Router (recommended)#
// 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#
// 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:
npm install raw-body
npm install -D @types/node
Python / Flask#
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
// 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#
# 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:
// 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:
// 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:
if (receivedSignature === expectedSignature) {
// ❌ Vulnerable to timing attacks
}
✅ CORRECT:
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:
// ❌ Hardcoded secret
const secret = 'a3f5c8d9e2b1f4a7...';
✅ CORRECT:
// ✅ 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:
// 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:
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:
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:
// 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#
- Get your webhook secret key
- Create test payload:
json
{"eventType":"leads.submit","chatbotId":"test","payload":{},"timestamp":"2025-11-26T10:30:00Z"} - 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); - 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#
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:
-
Using parsed body instead of raw body
- Solution: Get raw request body before parsing JSON
-
Wrong secret key
- Solution: Verify environment variable is correct
- Check for whitespace or newlines in secret
-
Body transformation
- Solution: Ensure no middleware is modifying the body
- Disable body parser for webhook route
-
Character encoding issues
- Solution: Use UTF-8 encoding for body and signature
Debugging:
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:
- Go to Agent Settings → Webhooks
- Click webhook → View Logs
- 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#
- Webhook Examples - Real-world integration examples
- Webhook Events - Available event types
- API Reference - Full API documentation