Webhook events reference
This page is the reference for all five webhook event types. For the how, see Connect Zapier or Connect Make.
Common envelope
Every event has the same envelope structure:
{
"event_id": "evt_8f3a...",
"event_type": "lead.captured",
"occurred_at": "2026-05-01T15:23:00Z",
"organization_id": "org_abc",
"chatbot_id": "bot_xyz",
"data": {
/* event-specific payload */
}
}Headers on every delivery:
Content-Type: application/jsonUser-Agent: Hilal-Chatbot-Webhooks/1.0X-Webhook-Signature: t=<unix>,v1=<hex>— HMAC-SHA256 oft.body.X-Webhook-Event-Id: evt_8f3a...— the sameevent_idas in the body.
The signature header lets you verify the payload came from Hilal Chatbot. See Verifying signatures below.
lead.captured (fully wired)
Fires when a user provides identifying information during a chat (email, phone, name).
data payload:
{
"lead_id": "lead_123",
"conversation_id": "conv_456",
"email": "user@example.com",
"name": "Jane Doe",
"phone": "+15551234567",
"captured_via": "user-info-form"
}Captured fields vary — only the ones you actually collected appear.
conversation.started (coming soon)
Fires when a new conversation begins, on any channel.
data payload:
{
"conversation_id": "conv_456",
"channel": "web",
"user_identity": {
"identitySource": "verified",
"sub": "user_789",
"email": "user@example.com"
}
}user_identity reflects JWT verification status if a token was passed; anonymous otherwise.
conversation.ended (coming soon)
Fires when a conversation closes — auto-closed by inactivity, manually closed by an agent, or resolved by the bot.
data payload:
{
"conversation_id": "conv_456",
"duration_seconds": 312,
"message_count": 8,
"status": "resolved",
"csat_score": 5
}escalation.triggered (coming soon)
Fires when the bot hands off to a live agent — either because the bot’s confidence dropped below the escalation threshold, the user explicitly asked, or an agent took over manually.
data payload:
{
"conversation_id": "conv_456",
"reason": "low_confidence",
"assigned_agent_id": null
}assigned_agent_id is filled when an agent picks up the conversation; for the initial escalation, it’s null.
message.received (coming soon — high volume)
Fires for every user message. Use carefully — a busy chatbot can produce thousands of these per hour.
data payload:
{
"conversation_id": "conv_456",
"message_id": "msg_789",
"role": "user",
"content": "How do I reset my password?",
"channel": "web"
}If you only need first messages, subscribe to conversation.started instead.
Verifying signatures
Every delivery includes X-Webhook-Signature: t=<unix-timestamp>,v1=<hex-signature>.
To verify in Node.js:
const crypto = require('crypto')
function verifyWebhookSignature(rawBody, signatureHeader, secret) {
const [tPart, v1Part] = signatureHeader.split(',')
const t = tPart.split('=')[1]
const v1 = v1Part.split('=')[1]
// Reject stale signatures (5 min skew window)
const now = Math.floor(Date.now() / 1000)
if (Math.abs(now - parseInt(t, 10)) > 300) return false
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex')
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
}The secret is shown once when you create the webhook — copy it and store in your env.
Retry behavior
If your endpoint returns non-2xx (or doesn’t respond within 15 seconds), Hilal Chatbot retries with exponential backoff:
- 1st retry: 30 seconds later
- 2nd retry: 2 minutes later
- 3rd retry: 10 minutes later
- After 3 failed retries: marked as failed; visible in the delivery log
For permanent failures (e.g., your endpoint is gone), pause the webhook in Integrations → Webhooks to stop retries.
Idempotency
Use the X-Webhook-Event-Id header to dedupe. Hilal Chatbot may deliver the same event twice (e.g., if a 2xx response was lost in transit). Treat each event_id as processed-once.
HTTPS only
The webhook URL must be HTTPS. Hilal Chatbot rejects HTTP URLs at registration.