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

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