Webhooks
Webhooks let your application receive real-time notifications when lead events happen, instead of polling the API.
How it works
- Subscribe to one or more event types by creating a webhook subscription (
POST /v1/webhook-subscriptions). - ABC Sales AI sends an HTTP
POSTto your callback URL whenever a subscribed event fires. - Your endpoint verifies the signature and responds with a
2xxstatus to acknowledge receipt.
Event types
You can subscribe to these event types:
| Event type | Fires when |
|---|---|
leads_created | A new lead is created |
leads_updated | A lead is updated |
lead_tag_added | A lead gains one or more tags it did not already carry |
leads_deleted | A lead is deleted |
leads_escalation_requested | A lead requests escalation to a human |
conversation_created | A new conversation is created |
Fetch the current list programmatically via GET /v1/webhook-subscriptions/events.
Managing subscriptions
| Action | Endpoint |
|---|---|
| List event types | GET /v1/webhook-subscriptions/events |
| Create a subscription | POST /v1/webhook-subscriptions |
| Update a subscription | PUT /v1/webhook-subscriptions/{subscriptionId} |
| Delete a subscription | DELETE /v1/webhook-subscriptions/{subscriptionId} |
| Regenerate signing secret | POST /v1/webhook-subscriptions/{subscriptionId}/regenerate-secret |
See the API Reference for exact request and response schemas.
Filter deliveries by lead tags
Set lead_tag_filter when creating or updating a subscription to receive events only for leads carrying any listed tag. Matching is case-insensitive, with at most 20 entries. For example, {"lead_tag_filter":["VIP","Interested"]} matches either tag, not necessarily both.
- On creation, omit the field or send
[]for no tag restriction. - On update, omitting the field preserves the existing filter; sending
[]clears it. - The filter checks tags carried by the lead when the delivery is built, not just the tags added by the triggering event.
- It is a delivery filter, not an authorization boundary. Do not assume this API option is exposed in the web interface.
Delivery headers
Every webhook request includes these headers:
| Header | Description |
|---|---|
X-AbcSalesAI-Signature | HMAC-SHA256 signature of the raw request body (hex). See below. |
X-AbcSalesAI-Event | The event type that triggered this delivery. |
X-AbcSalesAI-Delivery | Unique delivery ID (useful for logging and idempotency). |
X-AbcSalesAI-Timestamp | Unix timestamp of the delivery. |
Content-Type | application/json |
User-Agent | Engagematic-Webhook-Dispatcher/1.0 |
Payload
The request body is JSON with this shape (a lead event example):
{
"event_type": "leads_created",
"event_data": {},
"lead": {
"id": "external-lead-id",
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "+60123456789",
"source": "whatsapp",
"timezone": "Asia/Kuala_Lumpur",
"locale": "en",
"currency": "MYR",
"tags": ["new"],
"custom_fields": {},
"created_at": "2026-01-01T08:00:00Z"
}
}
event_data carries event-specific detail. conversation_created adds a top-level conversation object (below), lead_tag_added adds tags_added, and leads_escalation_requested may add escalation_reason and escalation_topic.
lead_tag_added
{
"event_type": "lead_tag_added",
"event_data": { "company_id": 123, "lead_id": 456, "tag_names": ["VIP"] },
"lead": { "id": "external-lead-id", "name": "Jane Doe", "tags": ["new", "VIP"] },
"tags_added": ["VIP"]
}
The lead object is shortened here. tags_added identifies newly gained tags; lead.tags is the lead's current tag set. Reapplying an existing tag is not a new addition, and removing a tag is not a lead_tag_added event. Tags removed before the delivery is built are not reported as newly added when the current tag set can be read.
conversation_created
{
"event_type": "conversation_created",
"event_data": { "company_id": 123, "lead_id": 456, "conversation_id": 789 },
"lead": { "id": "external-lead-id", "name": "Jane Doe" },
"conversation": {
"lead_id": 456,
"company_id": 123,
"conversation_id": 789,
"message": "Hi Jane! Yes, 3pm is still free. Shall I book it for you?",
"sent_by": "agent",
"message_source": "ai_agent"
}
}
(The lead object is shortened here; it has the same fields as in the lead events above.)
| Field | Description |
|---|---|
message | The message text. Can be empty, for example for an attachment-only message or a silent AI turn (below). |
sent_by | lead for a message from the lead, agent for anything sent on your side (AI or human). |
message_source | Where the message came from, when that is recorded (see below). Optional: left out when there is no source, never null. |
attachments | Files on the message. Fetch each download_url with your API key. Left out when there are none. |
message_source values you may see:
| Value | Meaning |
|---|---|
ai_agent | A reply written by your AI Employee |
ai_silent | The AI read the conversation and deliberately chose not to reply (for example, its instructions say to stay quiet after a human takes over). Nothing was sent to the lead, and message is empty. |
ai_reply_failed | The AI Employee turn failed without sending a customer-facing reply. This is not an intentional silence decision. |
human_agent | A reply from someone on your team in the ABC Sales AI inbox |
scheduled_message | A message sent by an automation or a scheduled message |
ai_manager | A message the AI Manager sent to the lead on a team member's instruction |
whatsapp_business_app, facebook_messenger_app, instagram_app | A message your team sent from the WhatsApp Business, Messenger or Instagram app, copied into ABC Sales AI |
facebook_comment, instagram_comment | A Facebook or Instagram comment |
Not every message records a source, so do not rely on message_source to tell AI and human replies apart. New values may be added; treat a value you do not recognise as an ordinary message.
conversation_created also fires for intentional silence and failed AI turns. If your integration only wants customer-facing messages, exclude events whose message_source is ai_silent or ai_reply_failed. Neither marker confirms delivery. Do not automatically resend a message solely because one of these markers arrives.
message_source remains optional; existing consumers can ignore it, and integrations should tolerate new values. The webhook does not include an internal no_reply_reason field.
Verifying signatures
Each subscription has a signing secret: a 64-character hex string, returned when you create the subscription and rotatable via the regenerate-secret endpoint.
The X-AbcSalesAI-Signature header is the HMAC-SHA256 of the raw request body, keyed with your signing secret, hex-encoded. Compute the same value and compare in constant time. Use the raw bytes of the body, before any JSON parsing or re-serialization.
const crypto = require('crypto');
function isValidSignature(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody) // the raw request body, not the parsed object
.digest('hex');
// constant-time comparison
return (
signatureHeader.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected))
);
}
Reject any request whose signature does not match.
Retries
If your endpoint does not respond with a 2xx status (or times out), ABC Sales AI retries delivery:
- Timeout: each attempt waits up to 10 seconds for your response.
- Success: any
2xxstatus. Anything else (including timeouts and network errors) is a failure. - Retries: up to 5 retries after the initial attempt, with exponential backoff of 1, 2, 4, 8, then 16 minutes.
- After the final retry fails, the delivery is marked failed and not retried again.
Make your handler idempotent (use X-AbcSalesAI-Delivery to de-duplicate), since a delivery can arrive more than once.