AI Chatbot Engine · Docs

Webhooks

Get leads, conversations and sync results in your CRM, helpdesk or spreadsheet as they happen. Add an endpoint in the dashboard under Settings → Webhooks (admins), choose its events, and send a test event to check it. Zapier’s and Make’s “catch hook” URLs work as endpoints.

Events

  • lead.created: A visitor left their details (form or typed in chat).
  • conversation.started: A visitor’s first message on your website or through the API.
  • conversation.ended: No messages for 30 minutes; includes a short summary.
  • source.sync_completed: A website or file source finished re-reading, with what changed.
  • source.sync_failed: A source couldn’t be read; includes the reason.
  • message.feedback: A visitor gave an answer a thumbs up or down, with their reason if any.
  • handoff.requested: A visitor asked for a person: their details and question, or the link they opened, with the conversation.
  • webhook.test: sent when you click “Send test event”.

What we send

POST /your/endpoint
Content-Type: application/json
X-Webhook-Event: lead.created
X-Webhook-Id: 0192f6c4-5b1e-7c3a-9d2f-6a8b7c1d2e3f
X-Signature: t=1790000000,v1=5f2c…

{
  "id": "0192f6c4-5b1e-7c3a-9d2f-6a8b7c1d2e3f",
  "type": "lead.created",
  "created_at": "2026-10-02T14:12:03.512Z",
  "org_id": "…",
  "agent_id": "…",
  "data": {
    "lead_id": "…",
    "conversation_id": "…",
    "fields": { "name": "Jane", "email": "jane@example.com" },
    "consented": true,
    "via": "form"
  }
}
  • Answer with any 2xx status within 10 seconds. Anything else is retried with growing gaps for about 24 hours.
  • An endpoint that fails for 3 days in a row is switched off, and your admins get an email. Switch it back on and resend from the delivery log.
  • A resent delivery keeps the same id, so you can ignore one you have already handled.
  • Endpoints must be https:// on the public internet.

Zapier and Make

No code needed: a webhook can start a Zap or a Make scenario, and from there reach thousands of apps.

  1. Zapier: create a Zap with the trigger Webhooks by Zapier → Catch Hook and copy the URL it shows. Make: add the module Webhooks → Custom webhook, create a webhook and copy its address.
  2. In the dashboard, Settings → Webhooks → Add a webhook: paste the address and choose the events, for example “Lead captured”.
  3. Click “Send test event”, then in Zapier “Test trigger” (or in Make “Redetermine data structure”), so it learns the fields.
  4. Add the next steps: a row in Google Sheets, a deal in your CRM, a message to your team.

Zapier and Make don’t check the signature; that’s fine for these addresses, which are secret. Treat them like passwords.

For Slack and HubSpot there are built-in integrations under Settings → Integrations.

Verify the signature

X-Signature is t=<unix time>,v1=<hex HMAC-SHA256>, computed with your endpoint’s signing secret over the timestamp, a dot and the raw body. Recompute it, compare in constant time, and reject timestamps older than a few minutes.

Node.js

import crypto from 'node:crypto';

// rawBody: the request body exactly as received (a string), before JSON parsing.
export function verifyWebhook(secret, rawBody, header, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // too old: maybe a replay
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const given = String(parts.v1 || '');
  return given.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}

Python

import hashlib
import hmac
import time


def verify_webhook(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
    """raw_body: the request body exactly as received, before JSON parsing."""
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts.get("t", "0"))
    if abs(time.time() - t) > tolerance:  # too old: maybe a replay
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Test vector

Your verification should accept exactly this (with the timestamp check relaxed):

secret     whsec_test_0123456789abcdefghijklmnopqrstu
timestamp  1790000000
body       {"id":"evt_1","type":"lead.created"}

header     t=1790000000,v1=62e917a576423ea0c58161adf53198b861c1b5753c50305fddaaabe4bc198754