> ## 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 conferences: create, manage, and end conference bridges

> What a conference bridge is, how to create one with POST /voice/conferences, dial in participants, update recording mode, end the room, watch the lifecycle webhooks, and bring an AI agent into a live conference.

# Voice conferences: create, manage, and end conference bridges

A conference bridge is a multi-party voice room: one named room that accepts
several concurrent call legs and mixes their audio so everyone hears everyone.
Use it for scheduled team calls, supervisor-led assistance desks, outbound
dial-out bridges where the host invites several guests, or any flow where a
1:1 call is not enough.

Every conference leg — the human participants you seed at creation and the
ones you invite afterward — exits over the Devotel softswitch. You never
configure a third-party outbound trunk to run a conference.

**Base path:** `/api/v1/voice`

**Authentication:** Clerk session or API key with `voice:read` for listing and
`voice:write` for every mutation.

The console surface for this lifecycle is the dashboard's
**Voice → Conferences** page ("create and manage voice conference bridges"),
where the room PIN, participant roster, per-leg status, and recording controls
are all visible without writing code. Everything below is the API surface that
page is built on.

## The lifecycle at a glance

A conference moves through a fixed lifecycle:

1. **Create** the room with `POST /voice/conferences` — optionally seeding
   dial-out participants in the same request.
2. **Join and leave** — each dialed participant lands on the room's
   participant roster with a per-leg status (`ringing`, `connected`,
   `no_answer`, `failed`, …).
3. **Manage** while the room is live — invite more participants, update the
   recording mode, mute/hold/kick legs, or add an AI agent.
4. **End** the room with `POST /voice/conferences/:id/end`, or let it close
   automatically when the last leg hangs up or the expiry backstop hits.

Each step emits audit rows and lifecycle webhooks, so you can follow the room
end-to-end. For diagnosing rooms that misbehave, see
[Troubleshooting: conference lifecycle failures](/troubleshooting/conference-failures).

## 1. What a conference bridge is

A conference is two resources working together:

* the **conference** row — the room itself, with a name, a status
  (`pending`, `in-progress`, `completed`, `failed`, `cancelled`), a join PIN,
  an optional host PIN, a recording mode, a participant cap, and a maximum
  lifetime; and
* the **participant** rows — one per dial-out leg, each with its own status,
  SIP response code, failure reason, join/leave timestamps, and (for GDPR)
  recording-consent state.

The creator's own inbound leg (for example, an operator dialing in from a PSTN
handset) attaches directly to the room and is not counted as a participant
row — participant rows represent the legs the platform dialed out to reach.

### PIN protection by default

When you omit the `pin` field at creation, the platform auto-generates a
random 6-digit PIN, so every conference is PIN-protected by default. You can
supply your own 4–8 digit numeric `pin`, plus an optional `host_pin` reserved
for future host-only DTMF controls. The create response echoes the PIN so the
operator can distribute it, and `POST /voice/conferences/:id/regenerate-pin`
rotates the PIN mid-call without ending the room.

PIN visibility is restricted: only the conference creator and users with the
owner or admin role read the raw PIN on list and detail responses. Every other
caller in your tenant sees the masked sentinel `***`.

## 2. Create a conference

`POST /api/v1/voice/conferences` creates the room and optionally seeds
dial-out participants in the same request. The create handler delegates to the
conference service, which persists the room, dials the seed legs in parallel,
and records each leg's outcome.

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/conferences" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Weekly compliance sync",
    "participants": ["+14155551234", "+19175550987"],
    "from": "+13035550100",
    "record": true,
    "pin": "4821"
  }'
```

| Field             | Type                       | Notes                                                                                                                                                                                                               |
| ----------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`            | string                     | Required, 1–200 characters.                                                                                                                                                                                         |
| `participants`    | array of E.164 strings     | 0–32 numbers to dial at creation. Formatted input (spaces, hyphens, parentheses) is normalized to canonical E.164 before dialing. Empty is fine — you can create the room empty and invite participants one by one. |
| `from`            | E.164 string, optional     | Caller ID for the dial-out legs; must be a number your tenant owns.                                                                                                                                                 |
| `record`          | boolean, optional          | Start recording when the room forms.                                                                                                                                                                                |
| `maxParticipants` | integer, optional          | Room cap, 1–32. Defaults to 8 when omitted.                                                                                                                                                                         |
| `pin`             | 4–8 digit string, optional | Join PIN. Auto-generated (6 digits) when omitted.                                                                                                                                                                   |
| `host_pin`        | 4–8 digit string, optional | Elevated host PIN; stays unset when omitted.                                                                                                                                                                        |

The response is `201 Created` with the conference row, including its `conf_<32
hex>` id and the active `pin`. Passing an `Idempotency-Key` header (or an
`idempotency_key` body field) makes create retries safe: within a five-minute
window, a duplicate key short-circuits to the original room instead of
spawning a second one.

Seed-leg failures are isolated, not fatal to the room: a leg that fails lands
on the roster as `failed` with its error code and SIP response code, and the
conference still exists for other legs. If every seed leg fails, the room is
kept for forensics at status `failed` — you receive no bridge, and the
troubleshooting page covers the error codes that explain why.

### Invite participants one by one

`POST /api/v1/voice/conferences/:id/participants` adds a single participant to
a live room. The target is a discriminated pair:

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_0123abcd/participants" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "target": { "kind": "pstn", "value": "+14155551234" } }'
```

Use `kind: "pstn"` with an E.164 number, or `kind: "sip"` with a `sip:` /
`sips:` URI. When the room carries a PIN, pass it in the `pin` field so the
invited leg can join.

## 3. End a conference

`POST /api/v1/voice/conferences/:id/end` terminates the room, dropping every
live leg:

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_0123abcd/end" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "end_reason": "wrap-up complete" }'
```

The optional `end_reason` (1–120 characters) is captured on the audit row. The
response is `{ "conference_id": "…", "status": "ended" }`. Ending the same
conference twice is safe — the terminal-state guard keeps the second request a
no-op rather than an error.

Rooms also end on their own when the last participant leaves, and a four-hour
expiry backstop closes any room that never torn down. Use the explicit end
call when you want the room closed on your schedule.

## 4. Update a conference

`PATCH /api/v1/voice/conferences/:id` updates the mutable fields on an
existing conference. Today that is the recording mode:

```bash cURL theme={null}
curl -X PATCH "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_0123abcd" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "record_mode": "auto_with_consent" }'
```

`record_mode` accepts `manual`, `auto_with_consent`, or `disabled`. In
`auto_with_consent` mode the platform collects each participant's join-time
recording consent, and `POST
/voice/conferences/:id/participants/:participantId/consent` with
`{ "state": "granted" }` or `{ "state": "declined" }` records the outcome per
leg. The patch response returns the updated conference row.

## 5. Read conferences and participants

`GET /api/v1/voice/conferences` lists your tenant's conferences newest-first,
paginated by start time with an opaque cursor:

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/voice/conferences?status=in-progress,completed&limit=50" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

* `status` — a comma-separated filter validated against the closed status
  enum; an unknown value returns 422. Unfiltered lists hide never-bridged
  dial-all-failed rooms (they remain available via an explicit
  `?status=failed` request).
* `limit` — 1–100, default 50.
* `cursor` — carry the response's `next_cursor` back in to fetch the next
  page.

Each row embeds a `total_participants` count so a list view does not need a
per-room follow-up.

`GET /api/v1/voice/conferences/:id` returns one conference with its
participant legs embedded under `participants` (handy for the detail page in a
single round-trip). For rooms where the roster grows, or when you only want
the legs, use the dedicated paginated endpoint
`GET /api/v1/voice/conferences/:id/participants?limit=100&cursor=…` — it
streams the participant list independently (limit up to 200).

Two rollups feed monitoring surfaces: `GET
/api/v1/voice/conferences/aggregate?window_days=30` returns the KPI strip
(total, active now, average duration, success rate, peak concurrent
participants, participants in window), and `GET
/api/v1/voice/conferences/failure-analytics?window_days=30` returns failure
rates with the top failure reasons and affected destinations. Both accept a
`window_days` capped at 365.

## 6. Lifecycle webhooks

Subscribe to these event types on your webhook endpoint to follow a room
without polling:

| Event                           | When it fires                                                                                                                                                                                                      |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `conference.created`            | The create call succeeded and the room exists. Carries the requested participant count so you can compare asks against joins.                                                                                      |
| `conference.participant_joined` | One leg answered and entered the room — once per successful leg.                                                                                                                                                   |
| `conference.participant_left`   | One leg disconnected, with duration, hangup reason, and the leg's last SIP response code.                                                                                                                          |
| `conference.ended`              | Exactly once per conference, always the last event. Carries `end_reason` (`all_legs_terminated`, `manual_end`, or `timeout`), `final_status` (`completed` or `failed`), and the room's lifetime participant count. |

`conference.ended` is deduplicated across both closing paths — when the last
leg leaves and an explicit end land close together, your endpoint still
receives exactly one event. Map the events to symptoms in
[Troubleshooting: conference lifecycle failures](/troubleshooting/conference-failures).

## 7. AI-agent participation

An AI agent can join a live conference as a participant — useful for
post-meeting summaries, silent note-taking, or an agent that assists the room
on request.

```bash cURL theme={null}
# Ask an agent to join
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_0123abcd/ai-agent" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "agent_id": "agent_0123456789" }'

# Give the agent a whisper note while it is in the room
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_0123abcd/ai-agent/whisper" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "whisper": "Collect action items only." }'

# List or remove the agent
curl "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_0123abcd/ai-agents" \
  -H "X-API-Key: dv_live_sk_your_key_here"

curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_0123abcd/ai-agent" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

The join request returns the agent's row id and call SID; the list endpoint
shows each agent's status (`joining`, `active`, `left`, `error`). Agent joins
are gated to owner/admin/developer roles, like the other conference control
mutations.

## Tenant-owned controls and ownership verification

Every participant-targeting mutation — kick, mute, unmute, hold, unhold, per-
participant consent — verifies that the conference and participant pair you
passed actually belongs to a conference owned by your tenant before the
platform forwards the control action. A leaked or guessed conference or call
identifier from another tenant cannot mute, kick, or join legs on your rooms,
and the verification is scoped per tenant, not per organization-wide flag.
Consents, PIN rotation, and end reasons are likewise per-tenant controls,
keeping the room's behavior inside your tenant's configured compliance
posture (GDPR recording consent, quiet-hours rules, and DNC checks all still
apply to the dial-out legs).

## Limits and limits-less behavior

* A conference holds at most **32 participants**; the create-time
  `maxParticipants` cap is 32 and defaults to 8 when omitted.
* Seed participants are bounded to **32** per create request; invite further
  legs one by one.
* List endpoints paginate with a cursor; single-conference detail embeds the
  first participants inline and the dedicated participants endpoint streams
  the rest.
* A room has a **four-hour expiry backstop**: a conference left running past
  its expiry is closed regardless, and the `conference.ended` webhook carries
  `end_reason: "timeout"` for that path.
* The KPI rollups accept a `window_days` up to 365.

No participant cap applies to historical conference reads — a room's roster is
fully retrievable after it ends, so post-hoc dispute or compliance review can
revisit who joined, who left, and which legs failed even after the four-hour
backstop closed the live room.

## Related

* [Troubleshooting: conference lifecycle failures](/troubleshooting/conference-failures) — map a misbehaving room to the lifecycle step that broke.
* [AI agent handback to human](/voice/ai-agent-handback) — the adjacent handoff pattern when an AI leg should hand back to a human participant.
* [Recording library](/voice/recording-library) — where conference recordings land and how signed playback URLs behave.
