Skip to main content

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 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: 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 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): 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).

5. Failure semantics

Every failed leg is attributed to one of three classes (the full model lives in conference failure-attribution 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). 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).
  • 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).
  • 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).

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. The family your consumers should branch on: 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