Webhook Security
Verify webhook signatures with HMAC-SHA256 to ensure requests come from AlonChat
Webhook Security#
Protect your webhook endpoints from unauthorized access using HMAC-SHA256 signature verification. This ensures webhook requests actually come from AlonChat and have not been tampered with.
Why Verification Matters#
Without signature verification, anyone who discovers your webhook URL can send fake events to your endpoint. With HMAC verification:
- Only authentic AlonChat requests are accepted
- Tampered payloads are rejected
- Combined with the delivery ID header, you can prevent replay attacks
HMAC-SHA256 Signature#
AlonChat signs every webhook payload with HMAC-SHA256 using your webhook's secret key.
How It Works#
- AlonChat takes the raw JSON request body
- Signs it with your webhook's secret key using HMAC-SHA256
- Includes the signature in the
x-alonchat-signatureheader - Your endpoint computes the same signature and compares
Signature Format#
x-alonchat-signature: sha256=a3f5c8d9e2b1f4a7c6d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
Components:
- Prefix:
sha256= - Algorithm: HMAC-SHA256
- Encoding: Hexadecimal
- Length: 64 hex characters (after the
sha256=prefix)
Additional Headers#
| Header | Purpose |
|---|---|
x-alonchat-signature | HMAC-SHA256 signature for verification |
x-alonchat-delivery-id | Unique ID for this delivery attempt (use for idempotency) |
User-Agent | AlonChat-Webhooks/1.0 |
Secret Key#
When you create a webhook, AlonChat generates a secret key:
whsec_a3f5c8d9e2b1f4a7c6d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
This key is shown only once during webhook creation. Store it securely:
- Use environment variables or a secrets manager
- Never commit it to version control
- Treat it like a password
If lost: Delete the webhook and create a new one to get a new secret.
Verification Examples#
Node.js / Next.js (App Router)#
// app/api/webhooks/alonchat/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
export async function POST(req: NextRequest) {
try {
// 1. Get raw body (before any parsing)
const rawBody = await req.text();
// 2. Get signature from header
const signature = req.headers.get('x-alonchat-signature');
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 401 }
);
}
// 3. Compute expected signature
const secret = process.env.ALONCHAT_WEBHOOK_SECRET;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// 4. Timing-safe comparison
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
if (!isValid) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// 5. Check for duplicate delivery
const deliveryId = req.headers.get('x-alonchat-delivery-id');
// Store and check deliveryId against your database/cache
// 6. Parse and process event
const event = JSON.parse(rawBody);
if (event.event === 'leads.submit') {
await handleLead(event.data);
}
return NextResponse.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Webhook error:', message);
return NextResponse.json(
{ error: 'Internal error' },
{ status: 500 }
);
}
}
Node.js / Next.js (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 signature = req.headers['x-alonchat-signature'] as string;
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
// 3. Compute expected signature
const secret = process.env.ALONCHAT_WEBHOOK_SECRET;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// 4. Timing-safe comparison
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 5. Process event
const event = JSON.parse(rawBody.toString());
if (event.event === 'leads.submit') {
await handleLead(event.data);
}
return res.status(200).json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Webhook error:', message);
return res.status(500).json({ error: 'Internal error' });
}
}
Install dependency:
npm install raw-body
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
signature = request.headers.get('x-alonchat-signature')
if not signature:
return jsonify({'error': 'Missing signature'}), 401
# 3. Compute expected signature
secret = os.environ.get('ALONCHAT_WEBHOOK_SECRET').encode()
digest = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
expected = f'sha256={digest}'
# 4. Timing-safe comparison
if not hmac.compare_digest(signature, expected):
return jsonify({'error': 'Invalid signature'}), 401
# 5. Process event
event = request.get_json()
if event['event'] == 'leads.submit':
save_to_crm(event['data'])
return jsonify({'success': True}), 200
PHP#
<?php
// 1. Get raw body
$rawBody = file_get_contents('php://input');
// 2. Get signature
$signature = $_SERVER['HTTP_X_ALONCHAT_SIGNATURE'] ?? '';
if (empty($signature)) {
http_response_code(401);
echo json_encode(['error' => 'Missing signature']);
exit;
}
// 3. Compute expected signature
$secret = getenv('ALONCHAT_WEBHOOK_SECRET');
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
// 4. Timing-safe comparison
if (!hash_equals($expected, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// 5. Process event
$event = json_decode($rawBody, true);
if ($event['event'] === 'leads.submit') {
save_to_crm($event['data']);
}
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
signature = request.headers['x-alonchat-signature']
if signature.blank?
render json: { error: 'Missing signature' }, status: :unauthorized
return
end
# 3. Compute expected signature
secret = ENV['ALONCHAT_WEBHOOK_SECRET']
digest = OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body)
expected = "sha256=#{digest}"
# 4. Timing-safe comparison
unless ActiveSupport::SecurityUtils.secure_compare(expected, signature)
render json: { error: 'Invalid signature' }, status: :unauthorized
return
end
# 5. Process event
event = JSON.parse(raw_body)
if event['event'] == 'leads.submit'
save_to_crm(event['data'])
end
render json: { success: true }, status: :ok
end
end
Idempotency#
AlonChat uses at-least-once delivery, which means events may be delivered more than once during retries. Each delivery includes a unique x-alonchat-delivery-id header.
Use this ID to prevent processing the same event twice:
// Production: use a database or Redis
const processedDeliveries = new Set<string>();
function handleWebhook(req) {
const deliveryId = req.headers['x-alonchat-delivery-id'];
if (processedDeliveries.has(deliveryId)) {
// Already processed, return success without re-processing
return { success: true, duplicate: true };
}
// Process event...
processedDeliveries.add(deliveryId);
return { success: true };
}
For production systems, store delivery IDs in a database or Redis with a TTL (e.g., 24 hours) instead of an in-memory Set.
Security Best Practices#
1. Always Use the Raw Body#
Compute the signature from the raw request body, not a re-serialized JSON object. JSON.stringify() may format the payload differently than the original, causing signature mismatches.
// WRONG - re-serialized body may not match
const body = req.body;
const sig = crypto.createHmac('sha256', secret)
.update(JSON.stringify(body))
.digest('hex');
// CORRECT - use raw body before parsing
const rawBody = await req.text();
const sig = crypto.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
2. Use Timing-Safe Comparison#
A regular === comparison is vulnerable to timing attacks. Always use a constant-time comparison function:
- Node.js:
crypto.timingSafeEqual() - Python:
hmac.compare_digest() - PHP:
hash_equals() - Ruby:
ActiveSupport::SecurityUtils.secure_compare()
3. Store Secrets Securely#
- Use environment variables or a secrets manager (AWS Secrets Manager, Vercel Environment Variables)
- Never hardcode secrets in source code
- Never commit secrets to version control
- Rotate secrets by deleting and recreating the webhook
4. Validate Payload Structure#
After verifying the signature, validate that the payload has the expected structure:
if (!event.event || !event.agent_id || !event.data) {
return res.status(400).json({ error: 'Invalid payload structure' });
}
5. Respond Quickly#
Return a 200 status as soon as possible. If your processing takes time, acknowledge the webhook immediately and process the event asynchronously (e.g., with a queue).
6. Use HTTPS#
AlonChat requires HTTPS for webhook endpoints (except localhost during development). This prevents the payload and headers from being intercepted in transit.
Troubleshooting#
"Invalid signature" errors#
Common causes:
- Using parsed body instead of raw body — get the raw request body before any JSON parsing middleware runs.
- Wrong secret key — verify the environment variable matches the key shown at webhook creation time. Check for trailing whitespace or newlines.
- Body transformation by middleware — ensure no middleware modifies the request body before your verification logic runs.
- Encoding mismatch — use UTF-8 encoding consistently.
Debugging checklist:
console.log('Received:', signature);
console.log('Expected:', expected);
console.log('Raw body length:', rawBody.length);
console.log('Secret length:', secret.length);
Webhook logs show 401#
Check your webhook logs in the AlonChat dashboard:
- Go to Settings > Webhooks
- Click your webhook > View Logs
- Look at the response details
If the signature keeps failing, delete the webhook and create a new one to get a fresh secret key.
Testing Verification#
Generate a Test Signature#
const crypto = require('crypto');
const payload = '{"event":"leads.submit","agent_id":"test","data":{},"timestamp":"2026-03-15T10:30:00Z"}';
const secret = 'your-webhook-secret';
const signature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
console.log(signature);
Send a Test Request#
curl -X POST https://your-endpoint.com/webhooks/alonchat \
-H "Content-Type: application/json" \
-H "x-alonchat-signature: sha256=<generated-signature>" \
-H "x-alonchat-delivery-id: test-001" \
-d '{"event":"leads.submit","agent_id":"test","data":{},"timestamp":"2026-03-15T10:30:00Z"}'
Next Steps#
- Webhook Examples — Real-world integration recipes
- Webhook Events — All event types and payloads
- Webhooks Overview — Setup and configuration