end-of-call-report event is triggered when a call completes and contains comprehensive analytics, recordings, cost breakdown, and AI-powered analysis results.
This is the most data-rich webhook event, containing full call metrics, analysis results, and recording information. It’s essential for post-call processing and analytics.
When It’s Triggered
This event is sent once per call when:- The call ends naturally (user or assistant hangs up)
- The call is terminated due to timeout
- The call is disconnected due to technical issues
- Call forwarding completes
Event Structure
Copy
{
"message": {
"timestamp": 1772702523935,
"type": "end-of-call-report",
"call": { /* Enhanced Call Object with metrics */ },
"assistant": { /* Assistant Object */ },
"messages": [ /* Complete conversation history */ ],
"phone": { /* Phone Object */ },
"customer": { /* Customer Object */ },
"analysis": { /* Complete Analysis Results */ }
}
}
Key Features
Enhanced Call Object
The call object includes additional fields only available at call completion:| Field | Type | Description |
|---|---|---|
endAt | string | ISO timestamp when call ended |
cost | object | Detailed cost breakdown by service |
metrics | object | Comprehensive call performance metrics |
recording | object | Recording file location and metadata |
Complete Analysis Results
Analysis Object
Theanalysis object contains AI-powered analysis results for the call.
Analysis data is primarily populated in
end-of-call-report events. Other events may contain empty analysis objects that will be filled once the call completes.| Field | Type | Description |
|---|---|---|
summary | string | AI-generated summary of the call conversation |
successEvaluation | string | Assessment of call success: “Excellent”, “Good”, “Fair”, “Poor” |
structuredData | object | Extracted structured data based on configured schema |
outcome | string | Call outcome classification |
guardrails | string | Guardrails evaluation result (or “DISABLED”) |
Summary
- Generated automatically if enabled in assistant configuration
- Provides 2-3 sentence overview of what happened during the call
- Uses configurable prompt and LLM model
Success Evaluation
- Determines if call met objectives based on assistant’s system prompt
- Uses descriptive scale: Excellent, Good, Fair, Poor
- Configurable evaluation criteria and rubric
Structured Data
- Extracts specific data points according to predefined JSON schema
- Commonly used for capturing:
- Customer information (name, phone, email)
- Appointment details
- Call metadata
- Custom business data
Example Structured Data Schema
Copy
{
"type": "object",
"properties": {
"Name": {
"type": "string",
"description": "Name of the patient"
},
"phone number": {
"type": "string",
"description": "Phone number of patient"
},
"appointment_date": {
"type": "string",
"description": "Scheduled appointment date"
}
},
"required": [
"Name"
]
}
Cost Breakdown
Detailed cost tracking across all services:| Field | Type | Description |
|---|---|---|
cost.callDuration | number | Total call duration in seconds |
cost.interactlyCost | number | Interactly platform fee |
cost.phoneCost | number | Telephony provider charges |
cost.ttsVendorCost | number | Text-to-speech generation costs |
cost.sttVendorCost | number | Speech-to-text transcription costs |
cost.llmVendorCost | number | Large language model API costs |
cost.totalCost | number | Sum of all costs |
Performance Metrics
Comprehensive metrics for optimization:- Audio Quality: Packet loss, delays, dropped chunks
- Response Timing: LLM, TTS, STT latency distributions
- Turn Statistics: User/bot turns, interruptions, barge-ins
- Error Tracking: Failed requests, retries, timeout counts
Example Payload
Copy
{
"message": {
"timestamp": 1772702523935,
"type": "end-of-call-report",
"call": {
"id": "WC-82015760-c3bd-427d-a23b-ba9b07e4ab85",
"status": "finished",
"startAt": "2026-03-05T09:21:20.063Z",
"endAt": "2026-03-05T09:22:01.739Z",
"assistantCallDuration": 41683,
"callEndTriggerBy": "bot",
"recording": {
"s3Bucket": "interactly-qa-us-recordings",
"path": "team-id/WC-82015760-c3bd-427d-a23b-ba9b07e4ab85.mp3"
},
"cost": {
"callDuration": 41.676,
"interactlyCost": 0.006946,
"phoneCost": 0.0165,
"ttsVendorCost": 0.010620,
"sttVendorCost": 0.005348,
"llmVendorCost": 0.0003385,
"totalCost": 0.0397529
}
},
"analysis": {
"summary": "User contacted Apollo clinic to book an appointment but ended the conversation abruptly.",
"successEvaluation": "Poor",
"structuredData": {
"Name": "",
"phone number": "",
"serial_number": 0
}
}
}
}
Common Use Cases
Call Analytics & Reporting
Copy
def process_call_analytics(event_data):
call = event_data["message"]["call"]
analysis = event_data["message"]["analysis"]
metrics = call.get("metrics", {}).get("callStats", {})
# Store comprehensive call record
call_record = {
"call_id": call["id"],
"duration_seconds": call["assistantCallDuration"] / 1000,
"end_trigger": call["callEndTriggerBy"],
"total_cost": call["cost"]["totalCost"],
"success_rating": analysis["successEvaluation"],
"summary": analysis["summary"],
"user_turns": metrics.get("turns", {}).get("user", 0),
"bot_turns": metrics.get("turns", {}).get("bot", 0),
"interruptions": metrics.get("turns", {}).get("interrupted", 0),
"avg_response_time": metrics.get("llmStats", {}).get("responseLatencyAvg", 0),
"stt_confidence": metrics.get("sttStats", {}).get("confidenceAvg", 0)
}
# Store in analytics database
analytics_db.call_records.insert_one(call_record)
# Generate insights
generate_performance_insights(call_record)
Recording Management
Copy
def handle_call_recording(event_data):
call = event_data["message"]["call"]
recording = call.get("recording", {})
if recording.get("s3Bucket") and recording.get("path"):
# Download recording for processing
recording_url = f"https://{recording['s3Bucket']}.s3.amazonaws.com/{recording['path']}"
# Store recording metadata
recording_record = {
"call_id": call["id"],
"s3_bucket": recording["s3Bucket"],
"s3_path": recording["path"],
"recording_url": recording_url,
"duration_ms": call["assistantCallDuration"],
"file_size": estimate_file_size(call["assistantCallDuration"]),
"created_at": datetime.utcnow()
}
db.recordings.insert_one(recording_record)
# Queue for additional processing
queue_recording_analysis(call["id"], recording_url)
CRM Integration
Copy
def sync_to_crm(event_data):
call = event_data["message"]["call"]
analysis = event_data["message"]["analysis"]
customer = event_data["message"]["customer"]
structured_data = analysis.get("structuredData", {})
# Extract customer information
customer_name = structured_data.get("Name", "")
customer_phone = structured_data.get("phone number", "")
if customer_name or customer_phone:
# Create or update contact in CRM
crm_contact = {
"name": customer_name,
"phone": customer_phone,
"last_interaction": call["endAt"],
"call_summary": analysis["summary"],
"call_success": analysis["successEvaluation"],
"call_duration": call["assistantCallDuration"],
"call_recording_url": get_recording_url(call["recording"])
}
crm_client.upsert_contact(customer_phone, crm_contact)
# Create activity record
crm_client.create_activity({
"contact_phone": customer_phone,
"type": "phone_call",
"subject": f"AI Assistant Call - {analysis['successEvaluation']}",
"body": analysis["summary"],
"duration_seconds": call["assistantCallDuration"] / 1000,
"date": call["endAt"]
})
Cost Tracking & Optimization
Copy
def track_call_costs(event_data):
call = event_data["message"]["call"]
cost = call["cost"]
metrics = call.get("metrics", {}).get("callStats", {})
# Store cost breakdown
cost_record = {
"call_id": call["id"],
"assistant_id": call["assistantId"],
"duration_seconds": cost["callDuration"],
"total_cost": cost["totalCost"],
"platform_cost": cost["interactlyCost"],
"phone_cost": cost["phoneCost"],
"tts_cost": cost["ttsVendorCost"],
"stt_cost": cost["sttVendorCost"],
"llm_cost": cost["llmVendorCost"],
"cost_per_minute": cost["totalCost"] / (cost["callDuration"] / 60),
"date": call["endAt"][:10] # Date for aggregation
}
db.costs.insert_one(cost_record)
# Analyze cost efficiency
tts_usage = metrics.get("ttsStats", {})
cache_hit_rate = tts_usage.get("cacheRequests", 0) / max(tts_usage.get("totalRequests", 1), 1)
# Alert on high costs or low cache efficiency
if cost["totalCost"] > COST_THRESHOLD:
alert_high_cost(call["id"], cost["totalCost"])
if cache_hit_rate < 0.5:
alert_low_cache_efficiency(call["assistantId"], cache_hit_rate)
Performance Monitoring
Copy
def monitor_call_performance(event_data):
call = event_data["message"]["call"]
metrics = call.get("metrics", {}).get("callStats", {})
# Check LLM performance
llm_stats = metrics.get("llmStats", {})
avg_response_time = llm_stats.get("responseLatencyAvg", 0)
if avg_response_time > LLM_LATENCY_THRESHOLD:
alert_performance_issue(
"slow_llm_response",
call["assistantId"],
avg_response_time
)
# Check STT quality
stt_stats = metrics.get("sttStats", {})
avg_confidence = stt_stats.get("confidenceAvg", 0)
if avg_confidence < STT_CONFIDENCE_THRESHOLD:
alert_performance_issue(
"low_stt_confidence",
call["assistantId"],
avg_confidence
)
# Check for audio quality issues
audio_stats = metrics.get("userAudio", {})
dropped_chunks = audio_stats.get("droppedChunksCount", 0)
if dropped_chunks > AUDIO_DROP_THRESHOLD:
alert_performance_issue(
"audio_quality_degradation",
call["id"],
dropped_chunks
)
Data Retention
Consider implementing data retention policies for end-of-call reports:Copy
def cleanup_old_call_data():
# Archive calls older than 90 days
cutoff_date = datetime.utcnow() - timedelta(days=90)
old_calls = db.call_reports.find({
"call.endAt": {"$lt": cutoff_date.isoformat()}
})
for call_report in old_calls:
# Move to archive storage
archive_call_report(call_report)
# Clean up large fields but keep summary
db.call_reports.update_one(
{"_id": call_report["_id"]},
{
"$unset": {
"messages": "",
"call.metrics": ""
}
}
)
End of Call Report Event Example
This is a complete example of an end-of-call-report webhook event payload.Copy
{
"message": {
"timestamp": 1772702523935,
"type": "end-of-call-report",
"call": {
"id": "WC-82015760-c3bd-427d-a23b-ba9b07e4ab85",
"teamId": "67c0231ae6880fe48ef929ee",
"assistantId": "697769ef5e6d94d5ad83e01e",
"callType": "web",
"direction": "inbound",
"startAt": "2026-03-05T09:21:20.063Z",
"endAt": "2026-03-05T09:22:01.739Z",
"userNumber": "web-Ramesh Naik",
"assistantNumber": "697769ef5e6d94d5ad83e01e",
"status": "finished",
"phoneCallStatus": "completed",
"phoneCallStatusReason": "Call is completed",
"callEndTriggerBy": "bot",
"assistantCallDuration": 41683,
"analysis": {
"summary": "",
"successEvaluation": "",
"guardrails": "DISABLED"
},
"recording": {
"s3Bucket": "interactly-qa-us-recordings",
"path": "67c0231ae6880fe48ef929ee/WC-82015760-c3bd-427d-a23b-ba9b07e4ab85.mp3"
},
"cost": {
"analysis": {
"total": 0
},
"callDuration": 41.676,
"ttsGenCharsTotal": 118,
"inputTokensTotal": 485,
"outputTokensTotal": 32,
"interactlyCost": 0.006946,
"phoneCost": 0.0165,
"ttsVendorCost": 0.010620000000000001,
"sttVendorCost": 0.005348418610800001,
"llmVendorCost": 0.0003385,
"totalCost": 0.0397529186108
},
"assistantOverrides": {
"dynamicVariables": {
"serial_number": ""
},
"variablesValidations": {
"serial_number": "none"
}
},
"metadata": {},
"metrics": {
"callStats": {
"userAudio": {
"delayedChunksCount": 70,
"latencyAvg": 105.05714285714286,
"latencyTotal": 7354,
"latencyIntervals": [
{
"max": 693,
"count": 12,
"avg": 151.75,
"duration": 8189,
"cumulativeDuration": 8189
},
{
"max": 113,
"count": 30,
"avg": 96.16666666666667,
"duration": 8000,
"cumulativeDuration": 16189
},
{
"max": 111,
"count": 25,
"avg": 97.6,
"duration": 7999,
"cumulativeDuration": 24188
},
{
"max": 88,
"count": 1,
"avg": 88,
"duration": 8000,
"cumulativeDuration": 32188
},
{
"max": 0,
"count": 0,
"avg": 0,
"duration": 8000,
"cumulativeDuration": 40188
},
{
"max": 64,
"count": 2,
"avg": 60,
"duration": 928,
"cumulativeDuration": 41116
}
],
"cumulativeDuration": 41116,
"droppedChunksCount": 0
},
"turns": {
"user": 4,
"bot": 4,
"played": 4,
"interrupted": 0,
"skipped": 0
},
"turnsStats": {
"userTurns": [
{
"start": 0,
"end": 3861,
"messageId": "user-1"
},
{
"start": 5760,
"end": 6786,
"messageId": "user-2"
},
{
"start": 7110,
"end": 11769,
"messageId": "user-3"
},
{
"start": 32970,
"end": 34289,
"messageId": "user-4"
}
],
"botTurns": [
{
"start": 223,
"end": 2131,
"interrupted": false,
"playedDuration": 1.908,
"audioDuration": 1.442,
"userMessageId": "welcome-1",
"stopSpeakingReason": null
},
{
"start": 15902,
"end": 20080,
"interrupted": false,
"playedDuration": 4.178,
"audioDuration": 3.207,
"userMessageId": "user-3",
"stopSpeakingReason": null
},
{
"start": 20081,
"end": 23159,
"interrupted": false,
"playedDuration": 3.078,
"audioDuration": 2.371,
"userMessageId": "user-3",
"stopSpeakingReason": null
},
{
"start": 39344,
"end": 41110,
"interrupted": false,
"playedDuration": 1.766,
"audioDuration": 1.303,
"userMessageId": "user-4",
"stopSpeakingReason": null
}
],
"botResponseTime": [
4133,
5055
],
"bargeInCount": 0,
"bargeInTimeAvg": 0
},
"ttsStats": {
"responseTimeDistribution": {
"0": 2,
"100": 0,
"200": 1,
"300": 1,
"600": 0,
"1000": 0,
"3000": 0,
"5000": 0,
"10000": 0
},
"totalRequests": 4,
"latencyAvg": 163.75,
"latencyTotal": 655,
"errors": 0,
"retries": 0,
"apiRequests": 2,
"apiLatencyTotal": 643,
"apiLatencyAvg": 321.5,
"cacheRequests": 2,
"cacheLatencyTotal": 12,
"cacheLatencyAvg": 6
},
"llmStats": {
"responseCount": 1,
"responseLatencyTotal": 2557.396173477173,
"responseLatencyAvg": 2557.396173477173,
"errors": 0,
"hangups": 0,
"responseTimeDistribution": {
"0": 0,
"100": 0,
"300": 0,
"600": 0,
"1000": 0,
"1500": 0,
"2000": 1,
"5000": 0,
"10000": 0
},
"inputTokens": 485,
"outputTokens": 32,
"inputTokensAvg": 485,
"outputTokensAvg": 32
},
"sttStats": {
"responseTimeDistribution": {
"0": 5,
"100": 0,
"200": 0,
"300": 0,
"600": 0,
"1000": 0,
"3000": 0,
"5000": 0,
"10000": 0
},
"responseCount": 5,
"responseLatencyAvg": -309.3994399999998,
"relativeLatencyAvg": -296.3994399999998,
"responseLatencyTotal": -1546.9971999999989,
"confidenceAvg": 0.6880248999999999,
"connectionRetries": 0,
"errors": 0,
"confidenceDistribution": {
"0": 1,
"0.1": 0,
"0.3": 0,
"0.5": 0,
"0.7": 3,
"0.9": 0
},
"eouProbabilityDistribution": {
"0": 0,
"0.1": 0,
"0.3": 0,
"0.5": 0,
"0.7": 0,
"0.9": 0
}
}
}
}
},
"assistant": {
"_id": "697769ef5e6d94d5ad83e01e",
"name": "Mary Dental - main"
},
"messages": [
{
"messageId": "welcome-1",
"role": "assistant",
"text": "Welcome to Apollo clinic!!",
"timestamp": 1772702480279
},
{
"messageId": "user-1",
"role": "user",
"text": "Hello.",
"timestamp": 1772702485138
},
{
"messageId": "user-2",
"role": "user",
"text": "Can you book an appointment?",
"timestamp": 1772702488063
},
{
"messageId": "user-3",
"role": "user",
"text": "Actually, never mind.",
"timestamp": 1772702493047
},
{
"messageId": "user-3-response-1772702495604",
"role": "assistant",
"text": "Hi there! I'd be happy to help you book an appointment with Dr. Sam.",
"timestamp": 1772702495958
},
{
"messageId": "user-4",
"role": "user",
"text": "Goodbye.",
"timestamp": 1772702520000
},
{
"messageId": "user-4-response-1772702521000",
"role": "assistant",
"text": "Thank you for calling Apollo clinic. Have a great day!",
"timestamp": 1772702521303
}
],
"phone": {
"provider": {
"name": ""
}
},
"customer": {
"number": "web-Ramesh Naik"
},
"analysis": {
"summary": "A user contacted Apollo clinic to book an appointment but decided against it and ended the conversation politely.",
"successEvaluation": "Fair",
"structuredData": {
"Name": "",
"phone number": "",
"serial_number": 0
},
"outcome": "No appointment booked"
}
}
}