> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting: conference lifecycle failures

> Diagnose conferences that start but never collect legs, legs that join and drop, rooms stuck in completed, and missing end reasons — map each symptom to the conference lifecycle events and the error code behind it.

# 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](/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:

1. **`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.
2. **`conference.participant_joined`** — one leg answered and entered the
   room. Fires once per successful leg, after the far end answers the dial-out.
3. **`conference.participant_left`** — one leg disconnected, with
   `duration_seconds`, a free-text `hangup_reason`, and the last
   `sip_response_code` for that leg.
4. **`conference.ended`** — exactly once, always the last event for a
   conference. It carries three fields that close the case:

   | Field               | What it tells you                                                                                                                                                   |
   | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
   | `end_reason`        | Why the room closed: `all_legs_terminated` (the last leg left), `manual_end` (a moderator or operator ended it), or `timeout` (the max-duration backstop closed it) |
   | `final_status`      | The room's terminal state: `completed` or `failed`                                                                                                                  |
   | `participant_count` | How many legs the room saw across its lifetime                                                                                                                      |

The healthy shape is exactly one `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](/reference/webhook-events).

## Symptom map

### The conference was created, but no participants ever joined

You receive `conference.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:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123" \
  -H "Authorization: Bearer $ORBIT_API_KEY"
```

Each participant carries a status (`failed`, `busy`, `no_answer`, …) and, on
failure, an error code and SIP response code. Match the code against the
[cause-and-fix table](#causes-and-fixes) below — a trunk problem is fixed on
the [SIP trunk page](/troubleshooting/sip-trunk), 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 receive `participant_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_reason` is free text from the carrier (`Normal Call Clearing`,
  `caller hangup`, `RTP timeout`). Match on substrings, never the exact
  string. `Normal Call Clearing` within 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: 200` with a near-zero `duration_seconds` is a clean
  answer-then-bye; the failure is on the destination side, not the trunk.
* An absent or non-`200` `sip_response_code` on a short leg points back at
  the carrier path — work it as a trunk issue on the
  [SIP trunk page](/troubleshooting/sip-trunk).

### 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 fires `conference.ended` with `end_reason:
  manual_end`. Never wait for the max-duration `timeout` backstop 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](/troubleshooting/webhook-deliveries).
  `conference.ended` is the only signal a room closed; polling the
  conference read endpoint for `status` is 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.

| Error code                                                                                | Cause                                                                                                                                   | Fix                                                                                                                                                                                                                       |
| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CONF_TRUNK_UNREGISTERED`                                                                 | The upstream voice trunk was unregistered or unreachable at dial time — transient                                                       | Retry in a few seconds. Persistent recurrence means the trunk registration itself is sick: work the [SIP trunk page](/troubleshooting/sip-trunk)                                                                          |
| `CONF_PROVIDER_ERROR`                                                                     | The voice provider returned a non-2xx that wasn't a validation or auth error                                                            | Retry shortly; if it recurs, capture the conference id and escalate with the per-leg SIP codes                                                                                                                            |
| `CONF_INVALID_PARTICIPANT`                                                                | A participant number didn't parse as E.164                                                                                              | Send numbers in international format (`+14155551234`)                                                                                                                                                                     |
| `CONF_NO_CALLER_ID`                                                                       | Neither the request's own caller ID nor a default resolved to a usable dial-from, so the create was refused before any room was written | Pick a voice-capable number from your inventory as the conference caller ID                                                                                                                                               |
| `CONF_CALLER_ID_NOT_OWNED` / `UNVERIFIED_CALLER_ID`                                       | The dial-from number isn't on your account, or isn't verified for outbound                                                              | Use a number from your own inventory and complete its verification                                                                                                                                                        |
| `CONF_INSUFFICIENT_CREDIT` (also seen as `INSUFFICIENT_BALANCE` / `INSUFFICIENT_CREDITS`) | The credit pre-charge for the dial was denied — insufficient balance or a spend cap                                                     | Top up the balance (see [Troubleshooting: insufficient balance](/troubleshooting/insufficient-balance)) or raise the spend cap, then retry                                                                                |
| `VOICE_BILLING_HOLD_FAILED`                                                               | The credit reservation itself failed in infrastructure — not a balance problem                                                          | Retry; it's transient by nature                                                                                                                                                                                           |
| `DNC_CONTACT` / `DNC_NUMBER` / `RECIPIENT_OPTED_OUT`                                      | The destination is on your Do-Not-Call list or opted out                                                                                | Remove the number from the participant list, or clear the suppression only where the contact's consent actually allows it — this is a tenant-owned compliance control                                                     |
| `TCPA_DIALING_WINDOW_BLOCKED` / `TCPA_TIMEZONE_UNKNOWN`                                   | Quiet-hours gate refused the leg for this recipient                                                                                     | Work the gate, not the conference: [Troubleshooting: dialing-window blocked calls](/troubleshooting/tcpa-window-blocked-calls)                                                                                            |
| `RMD_NOT_CURRENT`                                                                         | Reassigned-number check could not confirm the number                                                                                    | Re-verify consent freshness for that contact before dialing it                                                                                                                                                            |
| `VOICE_BLOCKED_DESTINATION`                                                               | The destination prefix is blocked (premium or restricted)                                                                               | Choose a different destination or remove the number                                                                                                                                                                       |
| `VOICE_RATE_LIMITED` / `VOICE_COUNTRY_RATE_LIMITED` / `VOICE_DAILY_SPEND_CAP`             | A fraud, rate, or spend gate throttled the leg                                                                                          | Space the dials, or raise the cap from your own limits where that's the gate                                                                                                                                              |
| `SENDING_BLOCKED` / `SENDING_PAUSED`                                                      | Outbound calling is held at the account level                                                                                           | Resolve the account-level hold first — no conference will dial until it clears                                                                                                                                            |
| `CONFERENCE_PIN_REQUIRED` / `CONFERENCE_PIN_MISMATCH`                                     | A join hit the room's PIN gate                                                                                                          | Supply the room PIN on the join path                                                                                                                                                                                      |
| `EMERGENCY_CALLING_NOT_SUPPORTED`                                                         | A participant number is an emergency short code                                                                                         | Conferences can't dial emergency numbers; remove it                                                                                                                                                                       |
| `CONFERENCE_DIAL_ALL_FAILED`                                                              | Aggregate: every leg on the create failed                                                                                               | Read the per-leg codes on the roster — this code is the sum of those, never the root cause                                                                                                                                |
| `SERVICE_UNAVAILABLE`                                                                     | Conference dialing isn't provisioned on the voice control plane yet                                                                     | Contact support — there is no self-serve fix                                                                                                                                                                              |
| Leg answered by voicemail or a call screener (not an error code)                          | Answering-machine detection classified the far end as a machine or screened call; the leg drops instead of joining                      | Watch for the `call.hit_voicemail` event on the leg's call id. Re-dial when a person is likely to answer; if machine answers are expected on your list, decide whether those numbers belong in the participant set at all |

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_failed` event type; a silent failure is read off
  the conference, not listened for.
* **`ended` always closes the series.** Any event gap before `ended`
  (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](/troubleshooting/webhook-deliveries)
  and [Troubleshooting: webhook event dedup](/troubleshooting/webhook-event-dedup)
  if a delivery replay shows duplicates instead.
* **An `ended` with no preceding `joined` events is valid.** It means zero
  legs ever connected — the create-time failure path above, not a lost event.

The room's own state is the fallback when events can't reconcile: the
conference read endpoint always reflects the current participant statuses
and the terminal `status`/`end_reason` once the room closes.

## See also

* [Troubleshooting: voice call quality](/troubleshooting/voice-call-quality)
  — one leg joined fine but the audio is the complaint.
* [Troubleshooting: SIP trunk](/troubleshooting/sip-trunk) — trunk
  registration and upstream failures behind `CONF_TRUNK_UNREGISTERED` and
  `CONF_PROVIDER_ERROR`.
* [Troubleshooting: insufficient balance](/troubleshooting/insufficient-balance)
  — the top-up path for `CONF_INSUFFICIENT_CREDIT` and its aliases.
* [Troubleshooting: dialing-window blocked calls](/troubleshooting/tcpa-window-blocked-calls)
  — the quiet-hours gate codes a conference leg can carry.
* [Troubleshooting: webhook deliveries](/troubleshooting/webhook-deliveries)
  — when the lifecycle is healthy but a lifecycle event never reached you.
* [Webhook events](/reference/webhook-events) — the full payload contract
  for every conference event.
* [Error codes](/reference/error-codes) — the HTTP-level conference error
  set this page works from.
