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_reasoncall_transfer). - A limit ends it:
max_duration_reached,inactivity,voicemail_reachedand 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_destinationand similar. These calls havecall_statusnot_connectedand no earliercall_started. - An inbound caller was queued over the concurrency limit and no slot opened (
concurrency_limit_reachedorno_concurrency_fallback).
Which settings enable it#
call_endedmust be in the agent'swebhook_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"])