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.
Webhook settings are available on Business and Enterprise plans. Active trials can test gated features during the trial, subject to trial limits.
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:
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.eventType === 'leads.submit') {
await handleLead(event.payload)
}
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.eventType === 'leads.submit') {
await handleLead(event.payload)
}
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.eventType || !event.chatbotId || !event.payload) {
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 =
'{"eventType":"leads.submit","chatbotId":"test","payload":{},"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 '{"eventType":"leads.submit","chatbotId":"test","payload":{},"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