> ## 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.

# Voice conference model: rooms, participants, and recordings

> Why conferences live in a dedicated per-tenant table rather than the call log, how a room moves from pending to in-progress to closed, how legs join, leave, and carry mute state, why a conference recording is one room-anchored artifact, and how rooms compose with park, ring groups, and supervisor takeover.

# Voice conference model: rooms, participants, and recordings

A conference on Orbit is a named room that bridges several call legs so
every participant hears every other participant. The [conferences
guide](/voice/conferences) walks the API step by step; this page defines
the object model underneath it: what a conference row is, how the room's
lifecycle advances, what a participant row tracks, how recording attaches
to the room rather than to a leg, and how conferences compose with park,
ring groups, and supervisor takeover. Read it before you build reporting
or webhook consumers on top of conference data.

Every outbound conference leg exits over the Devotel softswitch, the same
termination path as every other outbound call on the platform; you never
configure a third-party outbound trunk to run a conference. Inbound legs
(dial-in participants) arrive through your numbers like any other inbound
call.

## 1. A conference is not a call-log row

The platform stores each conference as its own row in a dedicated,
per-tenant `conferences` table, with one child row per leg in
`conference_participants`. The schema of record lives in
`packages/database/src/schema/tenant/conferences.ts`.

This shape is a locked design decision (BUILD-A-1, 2026-05-14) that
replaced a legacy encoding: a conference used to be a single row in the
call log with a conference type in its metadata and a JSON array of
participants. That encoding had three costs you could see from the
dashboard:

* The whole room rendered as one "Unknown" call row, with the caller id
  collapsed across legs and no status for the room itself.
* There was no per-participant timeline: who joined, when, and why a leg
  failed lived inside one opaque blob.
* Every per-leg lifecycle update rewrote the entire participant array, so
  a room's own state could not be queried or indexed directly.

What the dedicated table changed for you:

* **One row per room, one row per leg.** Per-leg statuses, join and leave
  times, and failure reasons are real rows with a check-constrained
  status enum, so the roster, the timeline, and failure analytics all read
  the same source of truth.
* **The room has its own status and duration.** `pending`,
  `in-progress`, `completed`, `failed`, and `cancelled` describe the room,
  independent of any single leg's outcome.
* **Recording state is first-class on the room.** The recording URL and
  the consent method that gated it sit on the conference row, not buried
  in per-leg metadata (see §4).
* **Legs cascade with the room.** When a conference is erased (for
  example on a data-erasure request), every participant row goes with it;
  no orphaned leg rows remain.

## 2. Room lifecycle

A conference moves through five statuses:

| Status | Meaning | What advances it |
| - | - | - |
| `pending` | The room exists; no leg has connected yet. | `POST /voice/conferences` succeeds. Seed dials are already in flight. |
| `in-progress` | At least one leg is bridged into the room. | The first leg connects. |
| `completed` | The room closed after at least one leg connected. | Last leg hangs up, an explicit end, or the expiry backstop. |
| `failed` | The room closed with every dialed leg failed. | The terminal roll-up, when no leg ever connected. |
| `cancelled` | The room was closed before it started. | An operator cancel while still `pending`. |

Three timers sit beside the state machine:

* **Expiry backstop.** Every room carries a hard lifetime, four hours by
  default, that you can set per conference at create time. A scheduled
  sweeper closes rooms past their expiry even if no terminal webhook ever
  fired for their legs, so a room whose legs all dropped silently cannot
  stay live forever.
* **Abandon cleanup.** A room whose create dialog was abandoned before
  any leg connected is reaped shortly after, rather than lingering in
  `pending`.
* **Duration.** The room's billable-style duration is the span from the
  start anchor to the end timestamp, computed on the row, so reports sort
  and filter on it directly.

### How participants join and leave

A leg reaches the room by one of three paths:

* **Seed dial-out** — participants listed in the create request are
  dialed in parallel as the room is persisted.
* **Later dial-out** — an invite after create dials one more leg into the
  live room.
* **Inbound dial-in** — a caller dials into the room and passes the PIN
  gate first: the platform collects the entered digits and verifies them
  before the conference verb bridges the leg. A wrong PIN never reaches
  the room.

Every conference is PIN-protected by default: omit the PIN at create and
the platform generates a random six-digit one; you may supply your own
4–8 digit PIN, plus an optional host PIN reserved for elevated
host-only controls. PIN rotation mid-call issues a new PIN without ending
the room.

A leg leaves when either side hangs up, when an operator kicks it, or
when the room ends. Leaving frees a seat: `max_participants` (default 8,
raiseable to 32 at create time) caps simultaneously-bridged legs, not the
total number of legs that may pass through the room.

Seed-leg failure is isolated, not fatal: a leg that comes back busy,
no-answer, rejected, or failed lands on the roster with its failure
reason and SIP response code while the room continues with the legs that
did connect. Only when **every** dialed leg fails does the room itself
close as `failed`.

### Mute and hand-raise

Mute is per-leg state: the participant row carries the mute flag, and it
is written only after the carrier accepts the mute or unmute command, so
the roster never claims a mute the media plane has not applied. Mute-all
and unmute-all write the same flag leg by leg.

Hand-raise is not part of the voice conference model. A raised-hand
queue exists for video rooms, where it drives the moderator's admit flow;
voice rooms intentionally carry only the mute flag as per-leg state. If
you are building a "who wants to speak" workflow on a voice room, model
it in your application on top of the participant roster rather than
expecting a hand-raise signal from the platform.

## 3. Two planes: signalling and media

A conference is two planes that advance independently.

**Signalling** is the tenant-side verb builder composing Jambonz verbs
per leg: for a dial-in leg it prepends the PIN-collecting step before the
conference verb; for a dial-out leg it dials and bridges on answer.
Per-leg lifecycle events from the voice engine (ringing, answered,
failed, hangup) propagate onto that leg's participant row, and the room's
own transitions (first connect, last disconnect, explicit end, expiry)
roll up from them.

**Media** is the room's shared mixer. Each connected leg carries an RTP
stream into the room and hears the mix of every other connected leg;
mixing happens in the platform's media plane, never at a participant's
device. The room's media surface is the Jambonz conference bridge backed
by the orbit-media service; the SFU you may know from [video
rooms](/concepts/video-room-model) is the video product's plane, and a
voice conference does not require a video room.

The split matters for control: **what you control is signalling-side and
tenant-owned** (PIN, participant cap, expiry, recording, mute), while the
media path itself is platform-owned. Nothing on this page configures a
provider route, because conferences never originate one.

## 4. Recording semantics: one artifact, anchored to the room

A conference recording is a **single artifact of the room**, not one
recording per leg. When recording stops, the conference row's recording
URL is populated and the file contains the room's mixed audio for the
recorded span.

Why the room is the right anchor:

* Legs come and go while the conversation continues. Leg-anchored
  recording would shard one conversation into as many overlapping files
  as participants, with no file that is "the meeting."
* The room is the unit of consent and of retention: one consent posture
  and one retention policy apply to the conversation, regardless of how
  many legs passed through it.
* The dashboard, the API, and your webhooks all reconcile one recording
  against one `conference_id`, instead of reconciling N leg recordings
  against N call ids.

Per-leg recording, by contrast, is the model for 1:1 calls (see
[call recording pipeline](/concepts/call-recording-pipeline)): the
recording attaches to the call because the call **is** the conversation.

Consent travels on both planes and stays one-to-one:

* The **room** records how consent was collected: verbal attestation,
  operator attestation, DTMF confirmation, or speech confirmation. These
  are the same methods the per-leg consent receipts use, so the room's
  consent method is a roll-up of the leg receipts, not a separate
  judgement.
* Each **leg** collects its own receipt at join time where the
  jurisdiction or your tenant configuration requires it, for example a
  pre-connect announcement or a DTMF confirmation on dial-in.
* Recording controls (start, stop, mode) are tenant-owned settings on the
  room, and where recorded audio is kept is likewise yours, including
  bring-your-own storage (see the [recording
  guide](/guides/voice-recording-byo-storage)).

## 5. Failure semantics

Every failed leg is attributed to one of three classes (the full model
lives in [conference failure-attribution
analytics](/voice/conferences-failure-analytics)):

* **Orbit-side** — refused before the call left the platform: unowned
  caller id, insufficient credit, unregistered trunk, a compliance gate.
* **Provider-routing** — broken in transit after hand-off: SIP 5xx,
  gateway rejection.
* **Destination-network** — the far end: busy, declined, no answer.

The participant status vocabulary separates outcomes cleanly: `failed`,
`busy`, `no_answer`, and `rejected` are failure terminals, while
`disconnected` is a connected-then-hung-up success. The room's terminal
status rolls up from that vocabulary with one rule: every dialed leg
failed means the room is `failed`; at least one leg connected means the
room is `completed`, however many other legs failed.

Three patterns recur in operations, and each reads differently on the
analytics surface:

* **Conference flip-pickup.** A participant is flipped out of the room
  into a directed 1:1 hand-off (the [call-flip
  console](/voice/conference-flip-pickup)). The leg's
  `participant_left` fires while the room continues; count it as a
  directed hand-off, never as a room or leg failure.
* **Late join.** A leg whose join lands well after the room started. The
  late joiner hears the live mix from the moment of joining, with no
  catch-up audio; a `participant_joined` far after `conference.created`
  is normal room behaviour, not a fault. Attribute late-join problems to
  the join path (dial-out latency, dial-in PIN retries) rather than to
  the room.
* **Scheduled-but-never-started.** A room created for a future time that
  no leg ever joins stays `pending` until the expiry backstop closes it.
  Your consumers see `conference.created` followed, eventually, by
  `conference.ended` with a participant count of zero. Treat that
  pairing as "scheduled but never started" and exclude it from
  failure-rate math: no leg was dialed, so nothing failed.

## 6. Composing with park, ring groups, and the supervisor plane

Conferences are the multi-leg primitive the rest of the voice floor
reuses.

* **Park and lobby.** Park is single-leg holding; a conference is
  multi-leg bridging. A parked call can supply the leg for a warm
  conference bridge (the retriever hears the announcement before the slot
  frees), and a conference leg that is parked leaves the room to shrink
  while the lobby keeps the held leg visible. The park slot's lifecycle
  is independent of the room's ([park and lobby
  model](/concepts/voice-call-park-lobby)).
* **Hunt and ring topologies.** To bring a ring group's members into a
  room, seed those members as dial-out participants at create time; the
  conference then **is** the group fan-out, with per-leg outcomes on the
  roster. The intercom-style composition (paging a group two-way into a
  room) falls back to one-way announcement until the conference-intercom
  path ships, so plan fan-out through seeded participants today (see
  [hunt and ring topologies](/concepts/hunt-and-ring-topologies)).
* **Supervisor plane.** Even a 1:1 answered call is a two-leg conference
  on the media plane, which is why monitor, whisper, barge, and takeover
  are all "add or shape a leg on the existing call's conference." A
  supervisor takeover or barge therefore turns the call into a 3-way
  conference; the supervisor's monitor leg joins in listen mode, audible
  to nobody. Supervisor verbs never originate outbound traffic (see
  [supervisor plane model](/concepts/voice-supervisor-plane-model)).

## 7. State vocabulary for webhook consumers

Conference lifecycle events ride the same delivery and retry semantics
as call events; the payloads live in the [webhook events
reference](/reference/webhook-events). The family your consumers should
branch on:

| Event | Fires when |
| - | - |
| `conference.created` | The create request succeeds; carries the room's cap and recording flag. |
| `conference.participant_joined` | A leg connects into the room (`200 OK` on that leg's invite). |
| `conference.participant_left` | A leg disconnects; carries duration, hangup reason, and SIP code. |
| `conference.ended` | The room closes. Always the last event for a conference id; carries the end reason and final status. |

Three rules keep consumers correct:

1. **Reconcile on `conference_id`, not arrival order.** Events for one
   room can interleave with events for other rooms and with `call.*`
   events for the same legs; the id is the join key.
2. **Never infer room membership from call statuses.** A leg entering a
   room does not change its call status in a way that encodes occupancy;
   that is exactly what the `conference.*` family exists for. Use call
   statuses for each leg's own outcome and conference events for the
   room.
3. **Branch on `final_status` and `end_reason` at the end.** The end
   event distinguishes the room that emptied (`all_legs_terminated`) from
   the operator-ended room (`manual_end`) from the expiry backstop
   (`timeout`), and reports `completed` versus `failed` as the terminal
   status. Apply events idempotently and let the end event close your
   record.

## See also

* [Voice conferences guide](/voice/conferences) — the step-by-step create, manage, and end surface
* [Conference failure-attribution analytics](/voice/conferences-failure-analytics) — the triage surface for failed legs
* [Voice call lifecycle](/concepts/voice-call-lifecycle) — the 1:1 leg state machine beside this page
* [Call park and the lobby](/concepts/voice-call-park-lobby) — single-leg holding and how it composes with rooms
* [Hunt and ring topologies](/concepts/hunt-and-ring-topologies) — fan-out models that feed conferences
* [Supervisor plane model](/concepts/voice-supervisor-plane-model) — monitor, whisper, barge, takeover as leg operations
* [Conference lifecycle troubleshooting](/troubleshooting/conference-failures) — per-code cause-and-fix for misbehaving rooms


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.