DialerBee APIv1 · 1.0.0-rc6

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"
}'

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

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"
    }
  }
}