Every event envelope has this top-level shape:
{
"event_id": "1f2e...-uuid",
"event_type": "call.ended",
"timestamp": 1744934400,
"data": { ... }
}
Below, each event’s data block is documented.
Call lifecycle
call.started
Fires when the dialer originates a call (pre-answer).
{
"call_id": "c-123",
"campaign_id": "camp-567",
"lead_id": "l-890",
"to_number": "9876543210",
"dial_time": "2026-04-18T14:37:02.419Z"
}
call.answered
Fires on CHANNEL_ANSWER — the callee picked up.
{
"call_id": "c-123",
"campaign_id": "camp-567",
"answer_time": "2026-04-18T14:37:08.102Z",
"ring_duration_s": 5
}
call.ended
Fires on CHANNEL_HANGUP. The most common event customers subscribe to.
{
"call_id": "c-123",
"campaign_id": "camp-567",
"lead_id": "l-890",
"end_time": "2026-04-18T14:39:02.102Z",
"hangup_cause": "NORMAL_CLEARING",
"talk_duration_s": 114,
"disposition": "interested",
"recording_url": null
}
recording_url is null on call.ended; wait for call.recording_ready
once the GCS upload completes (typically +2–5s after hangup).
call.recording_ready
Fires when the recording is uploaded and a signed URL is available.
{
"call_id": "c-123",
"recording_url": "https://storage.googleapis.com/yotel-recordings/...",
"duration_s": 114
}
The signed URL expires 7 days after issue. If you archive
recordings, pull them down promptly.
call.transcript_ready
Fires when Gemini transcription completes (typically +30–60s after
recording upload).
{
"call_id": "c-123",
"transcript_url": "https://storage.googleapis.com/yotel-transcripts/...",
"language": "ta-IN",
"duration_s": 114
}
Lead lifecycle
lead.completed
Fires when a lead reaches a terminal state (completed, failed,
dnd, or no_answer after exhausted retries).
{
"lead_id": "l-890",
"campaign_id": "camp-567",
"status": "completed",
"attempts": 2
}
Campaign lifecycle
campaign.started
{ "campaign_id": "camp-567" }
campaign.paused
Fires on BOTH manual and auto-pause. Distinguish via data.source:
source | Meaning |
|---|
"manual" | Operator paused via dashboard or /predictive/pause endpoint. user_id + reason included. |
"abandon_rate_exceeded" | D11 in-memory controller tripped the 60s sustained-breach threshold. |
"safety_sweep" | D13 Celery sweep’s DB-based fallback tripped the threshold. Runner was probably crashed. |
{
"campaign_id": "camp-567",
"source": "manual",
"reason": "Carrier complaint — investigate",
"user_id": "user-42"
}
campaign.resumed
Symmetric to paused. source is "manual" in all current paths
(auto-resume from safety-sweep doesn’t exist — humans re-enable).
{
"campaign_id": "camp-567",
"source": "manual",
"reason": "Abandon rate back under 1%",
"user_id": "user-42"
}
campaign.completed
{
"campaign_id": "camp-567",
"stats": {
"total_leads": 1250,
"connected": 847,
"abandoned": 31,
"connect_rate": 0.678
}
}
Agent lifecycle
agent.logged_in
{
"agent_id": "ag-7",
"extension": "1042",
"user_id": "user-42"
}
agent.logged_out
v2 events (coming)
These will land in v1.1 / v2:
| Event | Purpose |
|---|
campaign.created | CRM sync — register the campaign on your side |
lead.added | Post-create hook for just-added leads |
agent.state_changed | Fine-grained agent state stream (noisy — may be opt-in) |
supervisor.session_started | Barge / whisper / monitor start |