Operator webhooks

Webhook delivery reference

Adoomi sends signed HTTPS POST requests for operator events enabled in your notification matrix. Receivers should verify the signature, reject stale timestamps, and dedupe by event id.

Want to pipe these events into Slack, Google Sheets, a CRM, or 6,000+ other apps without writing a receiver? See the Connect to Zapier guide.

Headers

Signature

The signature is a sha256=-prefixed HMAC-SHA256 digest overtimestamp + "." + rawBodyusing the destination signing secret. Reject requests when the timestamp differs from your server clock by more than 300 seconds. During graceful rotation, verify eitherX-AIChat-Signature-256 with the current secret orX-AIChat-Signature-256-Prev with the previous secret. Store each X-AIChat-Event-Id and treat duplicates as already processed.

import crypto from 'node:crypto';

export function verifyAdoomiWebhook(req, rawBody, secret) {
  const timestamp = req.headers['x-aichat-timestamp'];
  const signature = req.headers['x-aichat-signature-256'];
  if (typeof timestamp !== 'string' || typeof signature !== 'string') return false;

  const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(ageSeconds) || ageSeconds > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const actual = signature.startsWith('sha256=') ? signature.slice(7) : signature;
  const actualBuffer = Buffer.from(actual, 'hex');
  const expectedBuffer = Buffer.from(expected, 'hex');
  if (actualBuffer.length !== expectedBuffer.length) return false;
  return crypto.timingSafeEqual(actualBuffer, expectedBuffer);
}

Payload envelope

Every delivery is a JSON envelope: a fixed event object (id, type, schema version, occurred_at, tenant_id, and a nullable bot_id) plus an event-specific data object. The X-AIChat-Event-Id header equals event.id, and X-AIChat-Event-Type equals event.type.

{
  "event": {
    "id": "b6f5a0c1-2d3e-4f50-8a1b-9c7d6e5f4a3b",
    "type": "new_lead",
    "version": "1.0",
    "occurred_at": "2026-05-14T09:15:30.000Z",
    "tenant_id": "7df8d8f2-0df7-48a8-9f52-5d42b4a67df0",
    "bot_id": "f9c3b0de-6f5b-4a22-bc8f-dac26351d6de"
  },
  "data": {
    "lead_id": "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7",
    "lead_name": "Sam Example",
    "lead_email": "sam@example.com",
    "lead_phone": "+15555550123",
    "lead_message": "Do you ship to Germany?",
    "page_url": "https://acme.example/pricing",
    "conversation_id": "d4c3b2a1-f6e5-4987-b1a0-7b6c5d4e3f2a",
    "chatbot_id": "f9c3b0de-6f5b-4a22-bc8f-dac26351d6de"
  }
}

Event types

The event.type field is one of the eight events below. Expand each for a representative data payload. Sample values are illustrative.

new_leadA visitor submitted a lead form. The webhook payload carries the contact details.
{
  "event": {
    "id": "b6f5a0c1-2d3e-4f50-8a1b-9c7d6e5f4a3b",
    "type": "new_lead",
    "version": "1.0",
    "occurred_at": "2026-05-14T09:15:30.000Z",
    "tenant_id": "7df8d8f2-0df7-48a8-9f52-5d42b4a67df0",
    "bot_id": "f9c3b0de-6f5b-4a22-bc8f-dac26351d6de"
  },
  "data": {
    "lead_id": "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7",
    "lead_name": "Sam Example",
    "lead_email": "sam@example.com",
    "lead_phone": "+15555550123",
    "lead_message": "Do you ship to Germany?",
    "page_url": "https://acme.example/pricing",
    "conversation_id": "d4c3b2a1-f6e5-4987-b1a0-7b6c5d4e3f2a",
    "chatbot_id": "f9c3b0de-6f5b-4a22-bc8f-dac26351d6de"
  }
}
escalationA conversation was handed off to a human (e.g. a WhatsApp handoff request).
{
  "event": {
    "id": "b6f5a0c1-2d3e-4f50-8a1b-9c7d6e5f4a3b",
    "type": "escalation",
    "version": "1.0",
    "occurred_at": "2026-05-14T09:15:30.000Z",
    "tenant_id": "7df8d8f2-0df7-48a8-9f52-5d42b4a67df0",
    "bot_id": "f9c3b0de-6f5b-4a22-bc8f-dac26351d6de"
  },
  "data": {
    "conversation_id": "d4c3b2a1-f6e5-4987-b1a0-7b6c5d4e3f2a",
    "bot_name": "Acme Support Bot",
    "reason": "WhatsApp handoff requested via widget"
  }
}
negative_feedbackA visitor gave a thumbs-down on an assistant reply.
{
  "event": {
    "id": "b6f5a0c1-2d3e-4f50-8a1b-9c7d6e5f4a3b",
    "type": "negative_feedback",
    "version": "1.0",
    "occurred_at": "2026-05-14T09:15:30.000Z",
    "tenant_id": "7df8d8f2-0df7-48a8-9f52-5d42b4a67df0",
    "bot_id": "f9c3b0de-6f5b-4a22-bc8f-dac26351d6de"
  },
  "data": {
    "conversation_id": "d4c3b2a1-f6e5-4987-b1a0-7b6c5d4e3f2a",
    "bot_name": "Acme Support Bot",
    "snippet": "The visitor down-voted: \"That did not answer my question.\""
  }
}
knowledge_gapThe bot repeatedly fell back / could not answer a recurring question.
{
  "event": {
    "id": "b6f5a0c1-2d3e-4f50-8a1b-9c7d6e5f4a3b",
    "type": "knowledge_gap",
    "version": "1.0",
    "occurred_at": "2026-05-14T09:15:30.000Z",
    "tenant_id": "7df8d8f2-0df7-48a8-9f52-5d42b4a67df0",
    "bot_id": "f9c3b0de-6f5b-4a22-bc8f-dac26351d6de"
  },
  "data": {
    "bot_name": "Acme Support Bot",
    "gap_summary": "Repeated unanswered questions about EU shipping timelines.",
    "sample_question_count": 7
  }
}
stale_sourceA knowledge source has not refreshed in a while and may be outdated.
{
  "event": {
    "id": "b6f5a0c1-2d3e-4f50-8a1b-9c7d6e5f4a3b",
    "type": "stale_source",
    "version": "1.0",
    "occurred_at": "2026-05-14T09:15:30.000Z",
    "tenant_id": "7df8d8f2-0df7-48a8-9f52-5d42b4a67df0",
    "bot_id": null
  },
  "data": {
    "source_name": "Acme Help Center",
    "source_url": "https://help.acme.example",
    "days_since_refresh": 31,
    "new_status": "stale"
  }
}
cap_approachingThe workspace crossed ~90% of its monthly message credits.
{
  "event": {
    "id": "b6f5a0c1-2d3e-4f50-8a1b-9c7d6e5f4a3b",
    "type": "cap_approaching",
    "version": "1.0",
    "occurred_at": "2026-05-14T09:15:30.000Z",
    "tenant_id": "7df8d8f2-0df7-48a8-9f52-5d42b4a67df0",
    "bot_id": null
  },
  "data": {
    "plan_name": "Standard",
    "used_credits": 1800,
    "total_credits": 2000,
    "percent_used": 90
  }
}
billing_failureA subscription invoice payment failed.
{
  "event": {
    "id": "b6f5a0c1-2d3e-4f50-8a1b-9c7d6e5f4a3b",
    "type": "billing_failure",
    "version": "1.0",
    "occurred_at": "2026-05-14T09:15:30.000Z",
    "tenant_id": "7df8d8f2-0df7-48a8-9f52-5d42b4a67df0",
    "bot_id": null
  },
  "data": {
    "invoice_url": "https://invoice.stripe.com/i/acct_123/test_abc",
    "amount": 49,
    "currency": "EUR",
    "next_attempt_at": "2026-05-18T09:15:30.000Z"
  }
}
subscription_cancelledA paid subscription ended; the workspace reverted to Free.
{
  "event": {
    "id": "b6f5a0c1-2d3e-4f50-8a1b-9c7d6e5f4a3b",
    "type": "subscription_cancelled",
    "version": "1.0",
    "occurred_at": "2026-05-14T09:15:30.000Z",
    "tenant_id": "7df8d8f2-0df7-48a8-9f52-5d42b4a67df0",
    "bot_id": null
  },
  "data": {
    "previous_plan_label": "Standard",
    "free_credits": 100
  }
}

Rotation

Rotate secret creates a new signing secret and keeps the previous secret valid for 24 hours, so receivers can deploy the new value without a delivery gap. Revoke now invalidates the previous secret immediately and clears in-flight delivery claims for that destination.

Adoomi