Troubleshooting: conference lifecycle failures
A conference moves through a fixed lifecycle:POST /api/v1/voice/conferences
creates the room, each dial-out leg produces a join or a failure, legs leave
one at a time, and the room closes once with a terminal end_reason. Every
step emits a webhook event, so a conference that “did nothing” always broke at
a specific step — and the events you did and did not receive tell you which
one.
This page maps each conference symptom to the step that failed. For a single
bad leg with audio trouble after a successful join, see
Troubleshooting: voice call quality;
this page works the room-level state machine, not per-call audio.
What the conference lifecycle tells you
Every conference emits the same four event types, in this order:-
conference.created— the create call succeeded and the room exists. Carries the requested participant count, so you can compare “how many legs were asked for” against how many ever joined. -
conference.participant_joined— one leg answered and entered the room. Fires once per successful leg, after the far end answers the dial-out. -
conference.participant_left— one leg disconnected, withduration_seconds, a free-texthangup_reason, and the lastsip_response_codefor that leg. -
conference.ended— exactly once, always the last event for a conference. It carries three fields that close the case:
created, N participant_joined, up to N
participant_left, and exactly one ended. Anything that deviates from that
shape is the symptom — find yours in the map below. The full payload for each
event is in Webhook events.
Symptom map
The conference was created, but no participants ever joined
You receiveconference.created, then nothing — no participant_joined, and
eventually conference.ended with final_status: "failed" or a
timeout end_reason.
The room was created but every dial-out leg failed before it could join.
Per-leg failures don’t produce a participant_joined event; they are recorded
on the conference’s participant roster with an error code instead. Fetch the
conference and read the legs:
failed, busy, no_answer, …) and, on
failure, an error code and SIP response code. Match the code against the
cause-and-fix table below — a trunk problem is fixed on
the SIP trunk page, a compliance block on the
contact or the gate, a balance block by topping up.
One subtle case: when every seed participant on the create request fails to
dial, the create can complete as a 201 while the returned data.status is
already failed with CONFERENCE_DIAL_ALL_FAILED. Check
status on the create response before treating a 201 as a healthy room —
an idempotent retry replays the same failed row as another 201.
A participant joined, then dropped seconds later
You receiveparticipant_joined and an immediate participant_left for the
same leg. The dial-out succeeded — the far end answered — but the leg was
torn down right after. Read the participant_left payload:
hangup_reasonis free text from the carrier (Normal Call Clearing,caller hangup,RTP timeout). Match on substrings, never the exact string.Normal Call Clearingwithin a second or two of join usually means the far end’s own equipment (an IVR, a spam screener, a PBX) answered and hung up.sip_response_code: 200with a near-zeroduration_secondsis a clean answer-then-bye; the failure is on the destination side, not the trunk.- An absent or non-
200sip_response_codeon a short leg points back at the carrier path — work it as a trunk issue on the SIP trunk page.
The conference sits in completed but the room never really ended — or a zombie room lingers
A room closes exactly once. If the active participant count reached zero but
no conference.ended arrived, the auto-close did not run; if the room shows
completed in the dashboard but your system still treats it as live, the
ended event was missed on your side.
- Room still live with zero participants: the auto-close rolls up the
room after the last leg disconnects, stamping
end_reason: all_legs_terminated. A room that stays non-terminal with an empty roster needs one thing from you: end it explicitly.POST /api/v1/voice/conferences/{id}/end(the moderator/operator end action) closes the room and firesconference.endedwithend_reason: manual_end. Never wait for the max-durationtimeoutbackstop to do this for you — it exists to close genuinely stuck rooms, not to order your workflow. - Your system missed the closure: subscribe your webhook endpoint to
conference.ended(or*) and replay missed deliveries from the webhook deliveries page — see Troubleshooting: webhook deliveries.conference.endedis the only signal a room closed; polling the conference read endpoint forstatusis the fallback, not the primary path.
There is no end_reason on a closed conference
Older closed conferences — and rooms closed through a path that stamped only
the room row — can carry no end_reason. Treat a missing end_reason as
“reason not recorded,” not as a distinct failure: derive the story from the
roster instead. If every participant is in a terminal state (disconnected,
left, completed), the room closed because everyone left — the same
conclusion all_legs_terminated would have carried. If every participant is
in a failure state (failed, busy, no_answer, rejected), the room
closed because every leg failed.
Conferences created today always stamp an end_reason; only do this
derivation for legacy rows.
Causes and fixes
Conference leg failures carry one of two kinds of codes. Conference-level codes (CONF_*, CONFERENCE_DIAL_ALL_FAILED) come from the create step or
the room as a whole. Per-leg gate codes come from the pre-dial checks on
an individual addParticipant dial and are stored on that participant as its
failure reason.
Distinguish trunk failover from bulk leg failure while you read the
roster: one leg failing with a
5xx SIP code or a trunk code is a route
problem on that leg; every leg failing with the same code points back at
the trunk or account-level gate (balance, send block), not at the individual
destinations. Fix the shared cause once, not one participant at a time.
A leg that fails with no SIP code at all (NO_ANSWER-class) never reached
SIP — it was refused by a pre-dial gate, so the SIP trunk page can’t help;
the gate’s code is the whole story.
When an event stops firing
Conference events follow strict sequencing per conference id:created is
always first, participant_joined only ever precedes the matching
participant_left for that leg, and ended is always last and fires exactly
once. If your integration depends on that order, hold these expectations:
- Per-leg failure never emits
participant_joined. A leg that is refused pre-dial or fails post-dial appears only on the conference roster — it has no join/left pair. “Leg count from webhooks” and “participant count requested” diverge by exactly the failed-leg count; reconcile through the read endpoint, not by counting events. - Failure events are roster data, not webhooks. There is no
conference.participant_failedevent type; a silent failure is read off the conference, not listened for. endedalways closes the series. Any event gap beforeended(a join you expected, a left you didn’t see) is a delivery gap, not a lifecycle gap — the conference service emits in order, and delivery is the layer that retries. Work it on Troubleshooting: webhook deliveries and Troubleshooting: webhook event dedup if a delivery replay shows duplicates instead.- An
endedwith no precedingjoinedevents is valid. It means zero legs ever connected — the create-time failure path above, not a lost event.
status/end_reason once the room closes.
See also
- Troubleshooting: voice call quality — one leg joined fine but the audio is the complaint.
- Troubleshooting: SIP trunk — trunk
registration and upstream failures behind
CONF_TRUNK_UNREGISTEREDandCONF_PROVIDER_ERROR. - Troubleshooting: insufficient balance
— the top-up path for
CONF_INSUFFICIENT_CREDITand its aliases. - Troubleshooting: dialing-window blocked calls — the quiet-hours gate codes a conference leg can carry.
- Troubleshooting: webhook deliveries — when the lifecycle is healthy but a lifecycle event never reached you.
- Webhook events — the full payload contract for every conference event.
- Error codes — the HTTP-level conference error set this page works from.