Skip to content
TriedDocs
Webhooks

call_ended

Sent when a call finishes or a dial attempt fails, with the full transcript, recording link, duration, cost and disconnection reason.

call_ended carries the finished call: transcript, recording, duration, cost and why it ended. Post-call analysis is not in it yet; that arrives in call_analyzed.

When it fires#

Once per call, when the call reaches a final state:

  • The caller or the agent hangs up.
  • The call is transferred (disconnection_reason call_transfer).
  • A limit ends it: max_duration_reached, inactivity, voicemail_reached and others.
  • You stop it with the API or the dashboard (manual_stopped).
  • An outbound dial fails before anyone answers: dial_busy, dial_no_answer, dial_failed, invalid_destination and similar. These calls have call_status not_connected and no earlier call_started.
  • An inbound caller was queued over the concurrency limit and no slot opened (concurrency_limit_reached or no_concurrency_fallback).

Which settings enable it#

  • call_ended must be in the agent's webhook_events. It is on by default.
  • A URL must resolve: the agent's webhook_url, else the workspace URL.

Payload#

{
  "event": "call_ended",
  "call": {
    "call_id": "call_3ffd3f3831b0d4e1a2c96f07",
    "call_type": "phone_call",
    "agent_id": "agent_5b0e2c7d9a1f4e3b8c6d2a10",
    "agent_version": 2,
    "agent_name": "Bondi Dental Front Desk",
    "call_status": "ended",
    "direction": "inbound",
    "from_number": "+61412345678",
    "to_number": "+61291234567",
    "metadata": {
      "sip_headers": { "x-clinic-id": "bondi-01" }
    },
    "retell_llm_dynamic_variables": {
      "x-clinic-id": "bondi-01",
      "business_name": "Bondi Dental"
    },
    "collected_dynamic_variables": {
      "patient_name": "Mia Chen"
    },
    "start_timestamp": 1760672019481,
    "end_timestamp": 1760672113681,
    "duration_ms": 94200,
    "transcript": "Agent: Hi, thanks for calling Bondi Dental, this is Ava. How can I help you today?\nUser: Hi, I'd like to book a check-up and clean.\nAgent: Sure. Can I get your name, please?\nUser: Mia Chen.\nAgent: Thanks Mia. I have Tuesday at 9am or Wednesday at 2pm. Which suits you?\nUser: Tuesday at 9 works.\nAgent: You're booked for Tuesday at 9am. See you then.",
    "transcript_object": [
      {
        "role": "agent",
        "content": "Hi, thanks for calling Bondi Dental, this is Ava. How can I help you today?",
        "words": [
          { "word": "Hi,", "start": 0.12, "end": 0.38 },
          { "word": "thanks", "start": 0.42, "end": 0.71 }
        ]
      },
      {
        "role": "user",
        "content": "Hi, I'd like to book a check-up and clean.",
        "words": [
          { "word": "Hi,", "start": 5.84, "end": 6.02 },
          { "word": "I'd", "start": 6.1, "end": 6.25 }
        ]
      },
      { "role": "agent", "content": "Sure. Can I get your name, please?", "words": [] },
      { "role": "user", "content": "Mia Chen.", "words": [] },
      { "role": "agent", "content": "Thanks Mia. I have Tuesday at 9am or Wednesday at 2pm. Which suits you?", "words": [] },
      { "role": "user", "content": "Tuesday at 9 works.", "words": [] },
      { "role": "agent", "content": "You're booked for Tuesday at 9am. See you then.", "words": [] }
    ],
    "transcript_with_tool_calls": [
      { "role": "agent", "content": "Hi, thanks for calling Bondi Dental, this is Ava. How can I help you today?", "ts": 1760672019702 },
      { "role": "user", "content": "Hi, I'd like to book a check-up and clean.", "ts": 1760672025330 },
      { "role": "agent", "content": "Sure. Can I get your name, please?", "ts": 1760672027010 },
      { "role": "user", "content": "Mia Chen.", "ts": 1760672031480 },
      {
        "role": "tool_call_invocation",
        "tool_call_id": "call_tool_01",
        "name": "cal_com_check_availability",
        "arguments": "{\"start\":\"2026-10-20\",\"time_zone\":\"Australia/Sydney\"}",
        "ts": 1760672032120
      },
      {
        "role": "tool_call_result",
        "tool_call_id": "call_tool_01",
        "content": "{\"slots\":[{\"start\":\"2026-10-21T09:00:00+11:00\"},{\"start\":\"2026-10-22T14:00:00+11:00\"}]}",
        "ts": 1760672032840
      },
      { "role": "agent", "content": "Thanks Mia. I have Tuesday at 9am or Wednesday at 2pm. Which suits you?", "ts": 1760672033400 },
      { "role": "user", "content": "Tuesday at 9 works.", "ts": 1760672040910 },
      { "role": "agent", "content": "You're booked for Tuesday at 9am. See you then.", "ts": 1760672044020 }
    ],
    "recording_url": "https://tried-recordings.s3.ap-southeast-2.amazonaws.com/ws_7d1c0a9e4b2f6a8c3e5d1b90/call_3ffd3f3831b0d4e1a2c96f07/mixed.ogg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=86400&X-Amz-Signature=4b1d...",
    "recording_multi_channel_url": null,
    "public_log_url": "https://api.tried.com.au/public/calls/call_3ffd3f3831b0d4e1a2c96f07/log?exp=1761276913900&sig=7a0d3f6c9b2e5a8d1c4f7b0e3a6d9c2f5b8e1a4d7c0f3b6e9a2d5c8f1b4e7a0d",
    "knowledge_base_retrieved_contents_url": "https://api.tried.com.au/public/calls/call_3ffd3f3831b0d4e1a2c96f07/kb-retrievals?exp=1761276913900&sig=2e5b8d1a4c7f0e3b6d9a2c5f8b1e4d7a0c3f6b9e2d5a8c1f4b7e0d3a6c9f2b5e",
    "stir_shaken_attestation": "A",
    "transfer_destination": null,
    "llm_token_usage": {
      "values": [1834, 1902, 2210, 2265],
      "average": 2053,
      "num_requests": 4
    },
    "disconnection_reason": "user_hangup",
    "call_analysis": null,
    "latency": {
      "e2e": { "p50": 742, "p90": 911, "p95": 911, "p99": 911, "max": 911, "min": 655, "num": 4, "values": [655, 742, 801, 911] },
      "llm": { "p50": 312, "p90": 455, "p95": 455, "p99": 455, "max": 455, "min": 270, "num": 4, "values": [270, 312, 340, 455] },
      "tts": { "p50": 168, "p90": 204, "p95": 204, "p99": 204, "max": 204, "min": 150, "num": 4, "values": [150, 168, 181, 204] }
    },
    "call_cost": {
      "product_costs": [
        { "product": "voice_infra", "unit_price": 0.08, "cost": 0.126667 },
        { "product": "gpt-5.6-terra", "unit_price": 0.1, "cost": 0.158333 },
        { "product": "voice_standard", "unit_price": 0.06, "cost": 0.095 }
      ],
      "total_duration_seconds": 95,
      "combined_cost": 0.38
    },
    "batch_call_id": null,
    "contact_id": "contact_6e2a9c1f4b7d0e3a5c8f1b24"
  }
}

Fields#

Every field is described on Call object. The ones that change at call_ended:

Field Type Description
call_status string ended, or not_connected when the call was never answered
end_timestamp integer When the call ended, Unix ms
duration_ms integer From start_timestamp to end_timestamp. 0 for not_connected calls
disconnection_reason string Why the call ended. See the table below
transcript string Plain text, one Agent: or User: line per turn
transcript_object array Turns with role, content and word timings in seconds
transcript_with_tool_calls array Turns plus tool calls, tool results, node transitions, DTMF digits, transfer events, agent swaps, SMS and injected context, each with ts
collected_dynamic_variables object Variables set during the call (extract variable tools, flow edges, SMS replies)
recording_url string or null Signed recording link, when recording is on and stored
recording_multi_channel_url string or null Signed dual-channel recording, when enabled
latency object Percentiles per stage (e2e, asr, llm, tts, knowledge_base, llm_websocket_network_rtt, s2s). Stages with no samples are left out
llm_token_usage object or null Tokens per LLM request, with average and count
call_cost object product_costs lines, total_duration_seconds (rounded up) and combined_cost in AUD
transfer_destination string or null The number the call was transferred to
call_analysis null Always null here. Arrives in call_analyzed

Disconnection reasons#

Reason Meaning
user_hangup, agent_hangup One side hung up
call_transfer The call was handed to a transfer target
voicemail_reached, ivr_reached Voicemail or a phone tree was detected
inactivity, max_duration_reached A call limit ended it
manual_stopped, call_take_over Stopped from the API or dashboard, or taken over in live monitoring
dial_busy, dial_failed, dial_no_answer, invalid_destination, user_declined Outbound dial outcomes
concurrency_limit_reached, no_concurrency_fallback No call slot was free
user_requested_dnc, user_requested_callback The caller asked not to be called, or asked for a callback
budget_reached, credit_exhausted, no_valid_payment Billing limits
scam_detected, marked_as_spam, network_blocked Trust and carrier blocks
registered_call_timeout, error_user_not_joined A registered or web call was never joined
telephony_provider_permission_denied, telephony_provider_unavailable, sip_routing_error Carrier or SIP errors
error_llm, error_asr, error_tts, error_no_audio_received, error_unknown, error_retell Platform or provider errors
error_llm_websocket_open, error_llm_websocket_lost_connection, error_llm_websocket_runtime, error_llm_websocket_corrupt_payload Custom LLM WebSocket errors

The full list with explanations is on Call object.

Data storage settings#

The webhook is built from the full call before the agent's data_storage_setting is applied. If the agent stores everything_except_pii or basic_attributes_only, your call_ended and call_analyzed payloads still contain the full transcript, and the stored copy is reduced afterwards. Treat your webhook receiver as the system of record for anything you need to keep.

How to respond#

Reply 2xx quickly and do heavy work (CRM writes, emails) in a queue. The reply body is ignored. Failed attempts are retried up to 3 times with 1, 2 and 4 second waits.

Post-call analysis starts at the same time as this delivery, but call_analyzed waits until this delivery finishes (including retries). A slow call_ended handler delays your call_analyzed by the same amount.

if payload["event"] == "call_ended":
    call = payload["call"]
    if call["call_status"] == "not_connected":
        schedule_retry(call["to_number"], reason=call["disconnection_reason"])
    else:
        store_transcript(call["call_id"], call["transcript_object"], call["recording_url"])