Events
Every webhook uses the same envelope with a typed data payload.
The envelope
Every body has the same top-level shape:
{
"id": "evt_abc123...", // unique per event, stable across retries
"type": "call.ended", // one of the event types below
"created_at": "2026-04-17T19:12:03.482Z",
"api_version": "v1",
"data": { ... } // payload — shape depends on type
}The envelope is versioned independently of the REST API. If we ever need to change its structure in a breaking way, api_version bumps and a deprecation window gets announced. See versioning.
Forward compatibility
- Ignore unknown top-level fields. We may add new envelope keys within v1. Don't throw on them.
- Ignore unknown fields inside
data. Same rule — parse what you care about, skip the rest. - Handle unknown event types. We may add new types. Match by prefix (
call.*) or fall back to an "unknown type" branch rather than crashing.
call.ended
Fires after every customer call completes that we classify as real engagement. Robocalls, IVR-style automated dialers, and wrong-number hangups are filtered out of this event so subscriber integrations don't ingest spam as customer activity. Such calls are still visible in the dashboard call list with an "unscorable" badge for audit; they simply do not trigger this webhook (or the related call.message_taken / call.callback_requested events).
{
"id": "evt_...",
"type": "call.ended",
"created_at": "2026-04-17T19:12:03.482Z",
"api_version": "v1",
"data": {
"call_id": "uuid",
"location_id": "uuid | null",
"started_at": "2026-04-17T19:06:12.000Z",
"ended_at": "2026-04-17T19:11:58.000Z",
"duration_seconds": 346,
"direction": "inbound",
"from_number": "redacted or null",
"disposition": "completed" | "hangup" | "transferred" | "escalated" | "missed",
"summary": "Caller rescheduled their 3pm appointment to Friday...",
"outcome_tags": ["reschedule", "appointment"],
"contact_id": "uuid | null",
"call_url": "https://allisonvoice.com/dashboard/calls/<id>",
// Order-taking extension. Present iff outcome === 'order'.
"outcome": "order",
"order_status": "new",
"order": {
"call_order_number": 247,
"profile_id": "uuid",
"profile_name": "Pickup",
"items": [
{
"catalog_item_id": "uuid",
"name": "Margherita pizza",
"quantity": 2,
"modifiers": [
{ "group_id": "uuid", "group_name": "Size", "option_name": "Large", "price_cents": 1800 }
],
"answers": [
{ "question_id": "uuid", "prompt": "Crust style", "answer": "thin" }
],
"unit_price_cents": 1800,
"line_total_cents": 3600
}
],
"order_answers": [
{ "question_id": "uuid", "prompt": "Pickup or delivery?", "answer": "pickup" }
],
"subtotal_cents": 3600,
"total_cents": 3600,
"fulfillment_estimate_value": 30,
"fulfillment_estimate_unit": "minutes",
"fulfillment_estimate_label": null,
"fulfillment_varies": false,
"expected_fulfillment_at": "2026-05-04T15:53:00Z",
"cancellation_allowed": true,
"cancellation_window_value": 15,
"cancellation_window_unit": "minutes",
"cancellation_deadline_at": "2026-05-04T15:38:00Z",
"placed_at": "2026-05-04T15:23:00Z",
"canceled_at": null,
"canceled_by": null,
"payment_status": null,
"external_order_id": null,
"payment_collected_at": null
}
}
}Transcripts are available via GET /v1/calls/{id} — we keep the webhook payload compact to fit well-behaved receivers. Subsequent order_status changes (fulfilled / canceled) fire a separate call_order.status_changed event.
call_order.status_changed
Fires when the order status mutates after the initial call.ended event. Sources: dashboard team mutation, public API mutation (PATCH /v1/calls/{id}/order-status), or a caller calling back to cancel via the agent. The canceled_by field disambiguates the source on cancellations.
{
"id": "evt_...",
"type": "call_order.status_changed",
"created_at": "2026-05-04T15:35:00Z",
"api_version": "v1",
"data": {
"call_id": "uuid",
"call_order_number": 247,
"previous_status": "new" | "in_progress" | "fulfilled" | "canceled" | null,
"new_status": "new" | "in_progress" | "fulfilled" | "canceled",
"changed_at": "2026-05-04T15:35:00Z",
"changed_by": "user_id | api_key_id | null",
"canceled_by": "team_via_dashboard" | "team_via_api" | "caller_via_phone" | null
}
}call.callback_requested
Caller asked for a human callback. Also sent in real time via email to configured notification recipients — the webhook lets you route callbacks through your own system as well.
{
"id": "evt_...",
"type": "call.callback_requested",
"created_at": "2026-04-17T19:08:44.118Z",
"api_version": "v1",
"data": {
"call_id": "uuid",
"location_id": "uuid | null",
"contact_id": "uuid | null",
"callback_number": "+1555XXXXXXX | null",
"requested_window": "asap" | "morning" | "afternoon" | "evening" | "custom",
"custom_window": "string | null",
"reason": "Caller wants to talk about financing options",
"requested_team_member_id": "uuid | null"
}
}call.message_taken
Caller left a message for a specific team member.
{
"id": "evt_...",
"type": "call.message_taken",
"created_at": "...",
"api_version": "v1",
"data": {
"call_id": "uuid",
"location_id": "uuid | null",
"contact_id": "uuid | null",
"for_team_member_id": "uuid | null",
"for_team_member_name": "Jane Smith | null",
"message": "Please call back about the Thursday delivery",
"caller_name": "John Doe | null",
"callback_number": "+1555XXXXXXX | null"
}
}call.escalation_triggered
An escalation rule matched during or after the call and the caller was routed to a human (transferred, SMS'd, or notified via your configured channel).
{
"id": "evt_...",
"type": "call.escalation_triggered",
"created_at": "...",
"api_version": "v1",
"data": {
"call_id": "uuid",
"location_id": "uuid | null",
"rule_id": "uuid",
"rule_name": "After-hours emergency",
"destination_type": "team_member" | "phone" | "email" | "sms",
"destination_id": "uuid | null",
"triggered_by": "keyword" | "llm_classification" | "caller_request",
"context": "Caller reported a water leak..."
}
}call.appointment_booked
A booking was created during the call (Allison confirmed it on-the-line).
{
"id": "evt_...",
"type": "call.appointment_booked",
"created_at": "...",
"api_version": "v1",
"data": {
"booking_id": "uuid",
"call_id": "uuid",
"calendar_id": "uuid",
"service_id": "uuid | null",
"service_name": "Consultation | null",
"location_id": "uuid | null",
"team_member_id": "uuid | null",
"start_at": "2026-04-22T15:00:00Z",
"end_at": "2026-04-22T15:30:00Z",
"timezone": "America/New_York",
"contact_id": "uuid | null",
"source": "allison_voice"
}
}contact.created
A new caller contact was auto-created from a call. Useful for syncing into your CRM as soon as someone reaches Allison for the first time. Only fires for calls classified as real engagement — same filter applied to call.ended. Robocall and IVR-style hangups don't create contacts.
{
"id": "evt_...",
"type": "contact.created",
"created_at": "...",
"api_version": "v1",
"data": {
"contact_id": "uuid",
"phone": "+1555XXXXXXX | null",
"name": "John Doe | null",
"email": "john@example.com | null",
"first_call_id": "uuid",
"first_call_at": "2026-04-17T19:06:12.000Z",
"source": "inbound_call"
}
}