Webhooks
Webhooks push events to your server as they happen, so you do not need to poll. DialerBee switches
webhooks on per account on request; until then the webhook operations answer 403
webhooks_disabled.
Set up an endpoint
curl -sS -X POST \
"https://app.dialerbee.com/api/v1/webhooks" \
-H "Authorization: Bearer $DIALERBEE_API_KEY" \
-H 'Idempotency-Key: 5f2b7c1e-8a3d-4c6f-9b0e-2d4a6c8e1f3a' \
-H "Content-Type: application/json" \
--data '{
"url": "https://crm.example.com/dialerbee/webhooks",
"events": [
"call.completed",
"disposition.logged"
],
"description": "CRM sync"
}'
- The URL must be
https://. Addresses that are not public on the internet are refused with 422webhook_url_refused; the URL is checked again before every delivery. - The answer contains the endpoint's signing
secret(whsec_...). It is shown only in that answer: store it in your secret store. DialerBee does not keep a copy it can show again: if you send the same request again with the sameIdempotency-Key, the replayed answer (Idempotency-Replayed: true) has the secret asnull. If you lost the secret, rotate it. - An account can have up to 10 endpoints.
- Send yourself a test event with
POST /v1/webhooks/{webhook_id}/test.
What a delivery looks like
Each delivery is a POST with a JSON body and these headers:
| Header | Value |
|---|---|
DialerBee-Signature |
sha256=<hex>: the HMAC-SHA256 of "<DialerBee-Timestamp>." + raw body, keyed with your secret. During a secret rotation, two values: sha256=<new>,sha256=<old>. |
DialerBee-Timestamp |
Unix seconds when this attempt was signed. |
DialerBee-Event-Id |
The event id (evt_...). Retries and replays reuse it: de-duplicate on it. |
DialerBee-Event |
The event type, for example call.completed. |
DialerBee-Delivery-Id |
This delivery (whd_...). A replay has a new delivery id. |
The body is an envelope with the event data:
{
"id": "evt_6f1c2a8e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"type": "call.completed",
"created_at": "2026-09-29T09:15:00Z",
"api_version": "v1",
"schema_version": 1,
"data": {
"call_id": "call_0f8fad5b-d9cb-469f-a165-70867728950e",
"direction": "outbound",
"status": "completed",
"from": "+12125550100",
"to": "+12125550123",
"campaign_id": "cmp_0f8fad5b-d9cb-469f-a165-70867728950e",
"started_at": "2026-09-29T11:58:00Z",
"answered_at": "2026-09-29T11:58:07Z",
"ended_at": "2026-09-29T11:59:40Z",
"duration_seconds": 93,
"hangup_cause": "NORMAL_CLEARING"
},
"call_details": {
"schema_version": 1,
"summary": {
"status": "ready",
"text": "Hello, this is Sara from Example Clinic. Is this a good time?",
"next_action": "example",
"language": "en",
"source": "call_analysis",
"generated_at": "2026-09-29T12:00:00Z",
"truncated": false
},
"disposition": {
"status": "ready",
"code": "interested",
"notes": "Prefers calls after 5 pm.",
"follow_up_at": "2026-09-29T12:00:00Z",
"logged_at": "2026-09-29T12:01:00Z"
}
}
}
The envelope never contains an internal account id. Ignore fields you do not know: new ones can be
added inside v1.
Verify every delivery
Verify the signature over the exact raw bytes you received, before you parse the JSON. Refuse a delivery signed more than 300 seconds from your clock, and compare signatures in constant time.
Python (standard library only):
import hashlib
import hmac
import time
def verify_dialerbee_webhook(raw_body, signature_header, timestamp_header, secrets,
now=None, tolerance=300):
"""True when the delivery was signed by DialerBee with one of `secrets`.
raw_body the exact bytes you received (never re-serialised JSON)
signature_header the DialerBee-Signature header value
timestamp_header the DialerBee-Timestamp header value
secrets your current secret, plus the previous one during a rotation
"""
if not signature_header or not timestamp_header or not timestamp_header.isdigit():
return False
timestamp = int(timestamp_header)
now = int(time.time()) if now is None else now
if abs(now - timestamp) > tolerance:
return False
received = []
for part in signature_header.split(","):
scheme, _, value = part.strip().partition("=")
if scheme == "sha256" and value:
received.append(value)
signed = timestamp_header.encode("ascii") + b"." + raw_body
for secret in secrets:
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
if any(hmac.compare_digest(expected, value) for value in received):
return True
return False
Node.js:
import { createHmac, timingSafeEqual } from 'node:crypto';
// True when the delivery was signed by DialerBee with one of `secrets`.
// rawBody: the exact bytes you received (a Buffer), never re-serialised JSON.
export function verifyDialerBeeWebhook(rawBody, signatureHeader, timestampHeader, secrets,
{ now = Math.floor(Date.now() / 1000), tolerance = 300 } = {}) {
if (!signatureHeader || !timestampHeader || !/^[0-9]+$/.test(timestampHeader)) return false;
if (Math.abs(now - Number(timestampHeader)) > tolerance) return false;
const received = signatureHeader.split(',')
.map((part) => part.trim().split('='))
.filter(([scheme, value]) => scheme === 'sha256' && /^[0-9a-f]{64}$/.test(value || ''))
.map(([, value]) => Buffer.from(value, 'hex'));
const signed = Buffer.concat([Buffer.from(timestampHeader + '.', 'ascii'), rawBody]);
for (const secret of secrets) {
const expected = createHmac('sha256', secret).update(signed).digest();
if (received.some((value) => value.length === expected.length && timingSafeEqual(value, expected))) {
return true;
}
}
return false;
}
The Python SDK does the same with dialerbee.webhooks.construct_event(...).
Answer quickly, process later
Answer with any 2xx within 10 seconds, then do the work. Anything else, or no answer in 10 seconds,
is retried after 1, 5, 15, 60 and 240 minutes; after that the delivery fails. Deliveries are
at least once, and can arrive out of order: de-duplicate on DialerBee-Event-Id and use
created_at to order.
History and replay
GET /v1/webhook-eventslists recent events with their deliveries and their status.POST /v1/webhook-events/{event_id}/replaysends an event again. The event keeps its id; the delivery gets a newwhd_id. Up to 5 replays per event per day.- Event and delivery history is kept for 30 days.
Rotating the secret
POST /v1/webhooks/{webhook_id}/rotate-secret returns a new secret,
once: a replayed answer has the secret as null, like the create answer.
For the next 24 hours DialerBee signs every delivery with both the new and the old secret, so you
can deploy the new one at your own pace: accept a delivery when either secret matches, then drop the
old secret.
Events
call.completed
A call ended (any direction)
{
"id": "evt_6f1c2a8e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"type": "call.completed",
"created_at": "2026-09-29T09:15:00Z",
"api_version": "v1",
"schema_version": 1,
"data": {
"call_id": "call_0f8fad5b-d9cb-469f-a165-70867728950e",
"direction": "outbound",
"status": "completed",
"from": "+12125550100",
"to": "+12125550123",
"campaign_id": "cmp_0f8fad5b-d9cb-469f-a165-70867728950e",
"started_at": "2026-09-29T11:58:00Z",
"answered_at": "2026-09-29T11:58:07Z",
"ended_at": "2026-09-29T11:59:40Z",
"duration_seconds": 93,
"hangup_cause": "NORMAL_CLEARING"
},
"call_details": {
"schema_version": 1,
"summary": {
"status": "ready",
"text": "Hello, this is Sara from Example Clinic. Is this a good time?",
"next_action": "example",
"language": "en",
"source": "call_analysis",
"generated_at": "2026-09-29T12:00:00Z",
"truncated": false
},
"disposition": {
"status": "ready",
"code": "interested",
"notes": "Prefers calls after 5 pm.",
"follow_up_at": "2026-09-29T12:00:00Z",
"logged_at": "2026-09-29T12:01:00Z"
}
}
}
ai_call.completed
A call handled by an AI agent ended, with its outcome
{
"id": "evt_6f1c2a8e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"type": "ai_call.completed",
"created_at": "2026-09-29T09:15:00Z",
"api_version": "v1",
"schema_version": 1,
"data": {
"call_id": "call_0f8fad5b-d9cb-469f-a165-70867728950e",
"direction": "outbound",
"from": "+12125550100",
"to": "+12125550123",
"ai_agent": "Front desk assistant",
"started_at": "2026-09-29T11:58:00Z",
"answered_at": "2026-09-29T11:58:07Z",
"ended_at": "2026-09-29T11:59:40Z",
"duration_seconds": 93,
"outcome": "appointment_booked",
"disposition_code": "interested",
"sentiment": "positive",
"requires_human_review": false,
"summary": "The customer wants a callback next week about the premium plan."
},
"call_details": {
"schema_version": 1,
"summary": {
"status": "ready",
"text": "Hello, this is Sara from Example Clinic. Is this a good time?",
"next_action": "example",
"language": "en",
"source": "call_analysis",
"generated_at": "2026-09-29T12:00:00Z",
"truncated": false
},
"disposition": {
"status": "ready",
"code": "interested",
"notes": "Prefers calls after 5 pm.",
"follow_up_at": "2026-09-29T12:00:00Z",
"logged_at": "2026-09-29T12:01:00Z"
}
}
}
disposition.logged
An agent logged a disposition for a call
{
"id": "evt_6f1c2a8e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"type": "disposition.logged",
"created_at": "2026-09-29T09:15:00Z",
"api_version": "v1",
"schema_version": 1,
"data": {
"call_id": "call_0f8fad5b-d9cb-469f-a165-70867728950e",
"campaign_id": "cmp_0f8fad5b-d9cb-469f-a165-70867728950e",
"lead_id": "lead_0f8fad5b-d9cb-469f-a165-70867728950e",
"disposition_code": "interested",
"notes": "Prefers calls after 5 pm.",
"phone": "+12125550101",
"logged_at": "2026-09-29T12:01:00Z"
},
"call_details": {
"schema_version": 1,
"summary": {
"status": "ready",
"text": "Hello, this is Sara from Example Clinic. Is this a good time?",
"next_action": "example",
"language": "en",
"source": "call_analysis",
"generated_at": "2026-09-29T12:00:00Z",
"truncated": false
},
"disposition": {
"status": "ready",
"code": "interested",
"notes": "Prefers calls after 5 pm.",
"follow_up_at": "2026-09-29T12:00:00Z",
"logged_at": "2026-09-29T12:01:00Z"
}
}
}
appointment.booked
An appointment was booked
{
"id": "evt_6f1c2a8e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"type": "appointment.booked",
"created_at": "2026-09-29T09:15:00Z",
"api_version": "v1",
"schema_version": 1,
"data": {
"appointment_id": "appt_0f8fad5b-d9cb-469f-a165-70867728950e",
"status": "confirmed",
"service": "Consultation",
"starts_at": "2026-10-01T14:00:00Z",
"timezone": "Asia/Amman",
"duration_minutes": 30,
"customer": {
"name": "Ann Lee",
"phone": "+12125550101"
},
"source": "api"
},
"call_details": {
"schema_version": 1,
"summary": {
"status": "ready",
"text": "Hello, this is Sara from Example Clinic. Is this a good time?",
"next_action": "example",
"language": "en",
"source": "call_analysis",
"generated_at": "2026-09-29T12:00:00Z",
"truncated": false
},
"disposition": {
"status": "ready",
"code": "interested",
"notes": "Prefers calls after 5 pm.",
"follow_up_at": "2026-09-29T12:00:00Z",
"logged_at": "2026-09-29T12:01:00Z"
}
}
}
appointment.moved
An appointment was moved to a new time
{
"id": "evt_6f1c2a8e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"type": "appointment.moved",
"created_at": "2026-09-29T09:15:00Z",
"api_version": "v1",
"schema_version": 1,
"data": {
"appointment_id": "appt_0f8fad5b-d9cb-469f-a165-70867728950e",
"status": "confirmed",
"service": "Consultation",
"starts_at": "2026-10-01T14:00:00Z",
"timezone": "Asia/Amman",
"duration_minutes": 30,
"customer": {
"name": "Ann Lee",
"phone": "+12125550101"
},
"source": "api"
},
"call_details": {
"schema_version": 1,
"summary": {
"status": "ready",
"text": "Hello, this is Sara from Example Clinic. Is this a good time?",
"next_action": "example",
"language": "en",
"source": "call_analysis",
"generated_at": "2026-09-29T12:00:00Z",
"truncated": false
},
"disposition": {
"status": "ready",
"code": "interested",
"notes": "Prefers calls after 5 pm.",
"follow_up_at": "2026-09-29T12:00:00Z",
"logged_at": "2026-09-29T12:01:00Z"
}
}
}
appointment.cancelled
An appointment was cancelled
{
"id": "evt_6f1c2a8e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"type": "appointment.cancelled",
"created_at": "2026-09-29T09:15:00Z",
"api_version": "v1",
"schema_version": 1,
"data": {
"appointment_id": "appt_0f8fad5b-d9cb-469f-a165-70867728950e",
"status": "confirmed",
"service": "Consultation",
"starts_at": "2026-10-01T14:00:00Z",
"timezone": "Asia/Amman",
"duration_minutes": 30,
"customer": {
"name": "Ann Lee",
"phone": "+12125550101"
},
"source": "api"
},
"call_details": {
"schema_version": 1,
"summary": {
"status": "ready",
"text": "Hello, this is Sara from Example Clinic. Is this a good time?",
"next_action": "example",
"language": "en",
"source": "call_analysis",
"generated_at": "2026-09-29T12:00:00Z",
"truncated": false
},
"disposition": {
"status": "ready",
"code": "interested",
"notes": "Prefers calls after 5 pm.",
"follow_up_at": "2026-09-29T12:00:00Z",
"logged_at": "2026-09-29T12:01:00Z"
}
}
}
webhook.test
Sent only by "Send test event"
{
"id": "evt_6f1c2a8e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"type": "webhook.test",
"created_at": "2026-09-29T09:15:00Z",
"api_version": "v1",
"schema_version": 1,
"data": {
"message": "This is a test event from DialerBee.",
"webhook_id": "wh_0f8fad5b-d9cb-469f-a165-70867728950e"
},
"call_details": {
"schema_version": 1,
"summary": {
"status": "ready",
"text": "Hello, this is Sara from Example Clinic. Is this a good time?",
"next_action": "example",
"language": "en",
"source": "call_analysis",
"generated_at": "2026-09-29T12:00:00Z",
"truncated": false
},
"disposition": {
"status": "ready",
"code": "interested",
"notes": "Prefers calls after 5 pm.",
"follow_up_at": "2026-09-29T12:00:00Z",
"logged_at": "2026-09-29T12:01:00Z"
}
}
}