CallEvent is how the gateway tells you what happened to a call after you hand it off. Events are pushed on the same uni-stream pipe as audio.

CallEvent schema

FieldTypeWhen populated
call_sidstringAlways.
event_typeEventTypeAlways (see table below).
statusstringCoarse string for human reading: INITIATED, RINGING, ANSWERED, COMPLETED, FAILED, BUSY, NO_ANSWER.
start_timestamp_msint64CHANNEL_CREATE and later events.
q850_causeint32CHANNEL_HANGUP_COMPLETE. ITU-T Q.850 cause.
recording_urlstringCHANNEL_HANGUP_COMPLETE if recording on.
duration_secondsint32CHANNEL_HANGUP_COMPLETE.
answer_timestamp_msint64CHANNEL_ANSWER and later.
end_timestamp_msint64CHANNEL_HANGUP_COMPLETE.
packets_sentuint32CHANNEL_HANGUP_COMPLETE.
packets_receiveduint32CHANNEL_HANGUP_COMPLETE.
packets_lostuint32CHANNEL_HANGUP_COMPLETE.
bytes_sentuint64CHANNEL_HANGUP_COMPLETE.
jitter_msdoubleCHANNEL_HANGUP_COMPLETE.
estimated_mosdoubleCHANNEL_HANGUP_COMPLETE. R-factor → MOS estimate.
trunk_idstringAlways.
tenant_idstringAlways.
codecstringNegotiated egress codec.
timestamp_msint64When the gateway emitted this event.
client_idstringEcho of EventStreamRequest.client_id.

Event types

event_type valueNameFired
0 UNKNOWNreservedShould never appear. If it does, log and drop.
1 CHANNEL_CREATEb-leg (callee) was created. Do not assume audio yet.
2 CHANNEL_ANSWERFar end picked up; audio is flowing.
3 CHANNEL_HANGUP_COMPLETEFinal accounting event. Includes Q.850 cause and stats.
4 CHANNEL_HOLDCaller pressed hold (or ExecuteDialplan(MUSIC_ON_HOLD)).
5 CHANNEL_RESUMEHold lifted.

Subscribing

Send one EventStreamRequest immediately after connect. The SDKs do this for you in connect() / connect_async(). The client_id should be a stable per-process UUID — the gateway uses it to demux events when the same tenant has multiple SDK instances connected.
[u32 length][u32 method_id=959835745][envelope: client_id="..."]
After this RPC, the gateway will open server-initiated uni-streams to your session containing audio and event frames.

Ordering

For a single call_sid, events are emitted strictly in this order:
CHANNEL_CREATE  →  (CHANNEL_ANSWER  →  CHANNEL_HOLD ⇄ CHANNEL_RESUME …)?  →  CHANNEL_HANGUP_COMPLETE
Across call_sids, events are not ordered — they’re just pushed as fast as the gateway can serialize them.

Reconnect behaviour

  • Within a session: If a uni-stream resets mid-frame, the gateway will open a new one and replay the latest CHANNEL_HANGUP_COMPLETE for any call that completed during the gap. Mid-call events are not replayed — there’s no point, you’ve already missed the audio.
  • Across sessions: A fresh QUIC connection with the same client_id resumes the subscription. The gateway holds an event buffer of 120 seconds per client_id; anything older is dropped.
Track in-flight call_sids in a set; remove them on CHANNEL_HANGUP_COMPLETE. Don’t try to drive UI off status strings alone — they’re for humans. Branch on event_type and use q850_cause to disambiguate hang-up reasons.
ACTIVE: set[str] = set()

def on_event(ev):
    if ev.event_type == EventType.CHANNEL_CREATE:
        ACTIVE.add(ev.call_sid)
    elif ev.event_type == EventType.CHANNEL_HANGUP_COMPLETE:
        ACTIVE.discard(ev.call_sid)
        if ev.q850_cause in (17, 18, 19):  # busy / no_user_response / no_answer
            schedule_retry(ev.call_sid)