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#

  1. AlonChat takes the raw JSON request body
  2. Signs it with your webhook's secret key using HMAC-SHA256
  3. Includes the signature in the x-alonchat-signature header
  4. Your endpoint computes the same signature and compares

Signature Format#

Code
x-alonchat-signature: sha256=a3f5c8d9e2b1f4a7c6d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0

Components:

  • Prefix: sha256=
  • Algorithm: HMAC-SHA256
  • Encoding: Hexadecimal
  • Length: 64 hex characters (after the sha256= prefix)

Additional Headers#

HeaderPurpose
x-alonchat-signatureHMAC-SHA256 signature for verification
x-alonchat-delivery-idUnique ID for this delivery attempt (use for idempotency)
User-AgentAlonChat-Webhooks/1.0

Secret Key#

When you create a webhook, AlonChat generates a secret key:

Code
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)#

typescript
// 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)#

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 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:

bash
npm install raw-body

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
    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
<?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)#

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
    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:

typescript
// 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.

javascript
// 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:

typescript
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:

  1. Using parsed body instead of raw body — get the raw request body before any JSON parsing middleware runs.
  2. Wrong secret key — verify the environment variable matches the key shown at webhook creation time. Check for trailing whitespace or newlines.
  3. Body transformation by middleware — ensure no middleware modifies the request body before your verification logic runs.
  4. Encoding mismatch — use UTF-8 encoding consistently.

Debugging checklist:

typescript
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:

  1. Go to Settings > Webhooks
  2. Click your webhook > View Logs
  3. 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#

javascript
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#

bash
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#