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": "+1XXXXXXXXXX or null when withheld",
"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>",
"trail": ["Find Bookings", "Check Availability", "Book Appointment"],
// 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. Recordings are finalized asynchronously after the call ends — wait for call.recording_ready before fetching playback URLs.
About trail
trail is a short array of caller-perspective labels in the order tools fired during the call, with consecutive duplicates collapsed. Render it as a Trail column with trail.join(' > '), a chip list, or a timeline. Empty when no trail-eligible tools fired (e.g., simple Q&A answered from foundational knowledge). Universal across industries — labels resolve from your custom integration name field plus a built-in label registry for platform tools.
Examples: ["Lookup Shipment", "Send Booking SMS"] (freight), ["View Menu", "Place Order"] (restaurant), ["Check Availability", "Book Appointment"] (service business). Same field, also returned on GET /v1/calls/{id}.
call.recording_ready
Fires once a call recording has been uploaded to Allison's storage and is ready to play back. This event is the safe signal to call GET /v1/calls/{id}/recording — fetching before this event arrives may 404 because the upload is still in progress. Only fires for organizations that have call recording enabled in their settings.
{
"id": "evt_...",
"type": "call.recording_ready",
"created_at": "2026-05-10T19:13:42.910Z",
"api_version": "v1",
"data": {
"call_id": "uuid",
"location_id": "uuid | null",
"duration_seconds": 346,
"recording_available": true
}
}The URL itself is not in the payload — call GET /v1/calls/{id}/recording to mint a fresh 1-hour presigned URL on demand. This avoids stored webhook delivery rows holding stale, expired URLs.
call.recording_ready often fires before call.ended because Twilio finalizes the recording faster than the post-call summarizer completes (10-20s gap is typical). Make your handlers order-independent: if your call.recording_ready handler can't find an existing row keyed by call_id, create one with just the recording flag set. The later call.ended will UPDATE that row with the rest of the call fields.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",
"destination_phone": "+1XXXXXXXXXX | 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"
}
}