Skip to content
Contact salesJoin the waitlist

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.

  • call.completed
  • call.missed
  • test

Version 1 allows additive optional fields. Ignore fields you do not recognize. Download the complete JSON schema.

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.

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.

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')));
}
import hashlib
import hmac
import re
import 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)
)

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

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.

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

Was this page helpful?