Webhook reference
Send call events to your own HTTPS endpoint. Configure the destination and signing secret in Integrations, then send a test delivery. Keep the secret on your server. Notification setup explains filters and delivery troubleshooting.
Events
Section titled “Events”call.completedcall.missedtest
Version 1 allows additive optional fields. Ignore fields you do not recognize. Download the complete JSON schema.
Headers and acknowledgement
Section titled “Headers and acknowledgement”| Header | Meaning |
|---|---|
| Content-Type | application/json |
| X-Xaller-Delivery-Id | Stable ID across delivery retries; use for deduplication |
| X-Xaller-Signature | t=<unix seconds>,v1=<HMAC-SHA256 hex> |
Verify before parsing. Save the delivery ID and your queued work atomically, then acknowledge with a 2xx status. Treat repeated IDs as successful duplicates. Do not assume events arrive in order. Return a non-2xx response if you cannot safely accept the event. Failed deliveries appear in the delivery log; check the log instead of relying on a fixed retry interval.
Signature verification
Section titled “Signature verification”The signed message is the timestamp, a period, and the original request bytes. Use the entire signing secret, including its whsec_ prefix, as UTF-8 text. Reject timestamps more than five minutes in either direction from your server clock. During the 24-hour secret rotation overlap, the header can contain multiple v1 signatures; accept a match against an active secret. Do not reserialize JSON before verification.
Node.js
Section titled “Node.js”import {createHmac, timingSafeEqual} from 'node:crypto';
// Pass the untouched request body as a Buffer, before JSON parsing.export function verifyWebhook(rawBody, signature, secret, now=Math.floor(Date.now()/1000)) { if (!Buffer.isBuffer(rawBody) || typeof signature!=='string') return false; const parts=signature.split(',').map(part=>part.trim()); const timestamps=parts.filter(part=>part.startsWith('t=')); if(timestamps.length!==1 || !/^t=\d{1,12}$/.test(timestamps[0])) return false; const timestamp=timestamps[0].slice(2); if(Math.abs(now-Number(timestamp))>300) return false; const expected=createHmac('sha256',secret).update(timestamp+'.').update(rawBody).digest(); return parts.filter(part=>/^v1=[a-fA-F0-9]{64}$/.test(part)).some(part=>timingSafeEqual(expected,Buffer.from(part.slice(3),'hex')));}Python
Section titled “Python”import hashlibimport hmacimport reimport time
def verify_webhook(raw_body: bytes, signature: str, secret: str, now=None) -> bool: """Verify raw request bytes before JSON parsing. Use the complete whsec_ secret.""" if not isinstance(raw_body, bytes) or not isinstance(signature, str): return False parts = [part.strip() for part in signature.split(",")] timestamps = [part[2:] for part in parts if part.startswith("t=")] if len(timestamps) != 1 or not re.fullmatch(r"[0-9]{1,12}", timestamps[0]): return False timestamp = timestamps[0] if abs((time.time() if now is None else now) - int(timestamp)) > 300: return False expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest() return any( hmac.compare_digest(expected, part[3:].lower()) for part in parts if re.fullmatch(r"v1=[a-fA-F0-9]{64}", part) )Fields
Section titled “Fields”Required means the property must exist; nullable properties can still contain null.
| Field | Type or values | Required | Description |
|---|---|---|---|
id |
string | Yes | Delivery ID, equal to the X-Xaller-Delivery-Id header. The same across retries, so receivers can de-duplicate. |
type |
“call.completed”, “call.missed”, “test” | Yes | |
version |
1 | Yes | |
created_at |
string | Yes | |
data |
object | Yes | |
data.call_id |
string or null | Yes | Null for test deliveries. |
data.account_id |
string | Yes | |
data.content |
“full”, “minimal” | Yes | minimal: HIPAA-mode accounts without a signed PHI attestation for this endpoint. Content fields are then absent. |
data.started_at |
string | Yes | |
data.ended_at |
string or null | Yes | |
data.duration_seconds |
integer or null | Yes | |
data.mode |
“phone”, “test” | Yes | |
data.language |
string or null | Yes | en or es |
data.outcome |
“new_lead”, “message_taken”, “question_answered”, “appointment_request”, “existing_customer”, “wrong_number”, “spam”, “hang_up”, “other”, null | Yes | |
data.sentiment |
“positive”, “neutral”, “negative”, null | Yes | |
data.urgency |
“low”, “normal”, “high”, “emergency”, null | Yes | |
data.follow_up_required |
boolean or null | Yes | |
data.summary_unavailable |
boolean | No | |
data.missed_reason |
string or null | No | For call.missed: why the agent couldn’t answer (agent_paused, trial_minutes_exhausted, payment_overdue, …). |
data.dashboard_url |
string | Yes | Opens the call in Xaller; requires sign-in. |
data.headline |
string or null | No | |
data.summary |
string or null | No | |
data.next_step |
string or null | No | |
data.caller_name |
string or null | No | |
data.caller_number |
string or null | No | E.164 |
data.returning_caller_count |
integer | No | |
data.details |
object | No | |
data.details.callback_number |
string or null | No | |
data.details.reason |
string or null | No | |
data.details.requested_service |
string or null | No | |
data.details.preferred_callback_time |
string or null | No | |
data.details.qualification_answers |
array | No | |
data.details.qualification_answers[].question |
string | Yes | |
data.details.qualification_answers[].answer |
string | Yes | |
data.transcript |
array | No | |
data.transcript[].speaker |
“caller”, “agent” | Yes | |
data.transcript[].text |
string | Yes | |
data.transcript[].start_ms |
integer or null | No | |
data.transcript[].end_ms |
integer or null | No | |
data.transcript_truncated |
boolean | No |
Content modes
Section titled “Content modes”minimal payloads omit caller identity, summaries, details, and transcripts. Retrieve permitted information from the dashboard. Full payloads may include personal information: restrict destination access and retention. A missing optional field is not an error.
Examples
Section titled “Examples”call.completed (full)
Section titled “call.completed (full)”{ "id": "00000000-0000-4000-8000-000000000001", "type": "call.completed", "version": 1, "created_at": "2026-10-11T15:00:00Z", "data": { "call_id": "00000000-0000-4000-8000-000000000002", "account_id": "00000000-0000-4000-8000-000000000003", "content": "full", "started_at": "2026-10-11T14:58:00Z", "ended_at": "2026-10-11T15:00:00Z", "duration_seconds": 120, "mode": "phone", "language": "en", "outcome": "message_taken", "sentiment": "neutral", "urgency": "normal", "follow_up_required": true, "dashboard_url": "https://app.xaller.ai/calls/00000000-0000-4000-8000-000000000002", "headline": "A caller requested a callback", "summary": "A caller asked about service availability.", "next_step": "Return the call." }}call.missed (minimal)
Section titled “call.missed (minimal)”{ "id": "00000000-0000-4000-8000-000000000001", "type": "call.missed", "version": 1, "created_at": "2026-10-11T15:00:00Z", "data": { "call_id": "00000000-0000-4000-8000-000000000002", "account_id": "00000000-0000-4000-8000-000000000003", "content": "minimal", "started_at": "2026-10-11T14:58:00Z", "ended_at": "2026-10-11T15:00:00Z", "duration_seconds": 120, "mode": "phone", "language": "en", "outcome": null, "sentiment": null, "urgency": null, "follow_up_required": null, "dashboard_url": "https://app.xaller.ai/calls/00000000-0000-4000-8000-000000000002" }}test (minimal)
Section titled “test (minimal)”{ "id": "00000000-0000-4000-8000-000000000001", "type": "test", "version": 1, "created_at": "2026-10-11T15:00:00Z", "data": { "call_id": null, "account_id": "00000000-0000-4000-8000-000000000003", "content": "minimal", "started_at": "2026-10-11T14:58:00Z", "ended_at": "2026-10-11T15:00:00Z", "duration_seconds": 120, "mode": "test", "language": "en", "outcome": null, "sentiment": null, "urgency": null, "follow_up_required": null, "dashboard_url": "https://app.xaller.ai/calls/00000000-0000-4000-8000-000000000002" }}