Conferences
Host multi-party voice rooms: bridge many call legs into one mixed audio space, dial new people in, mute and hold individuals, broadcast audio or text-to-speech to everyone, record the whole room, and let a supervisor whisper, monitor, or barge in for call-center coaching.
All paths under
/v1/.Authorization: Bearer <api-key>required. JSON in, JSON out,snake_case. Send anIdempotency-Keyon every POST that creates or dials.
Concepts
A conference is a server-side mixing room. You don't dial a conference number; you create the room on top of an existing answered call (the host leg, "leg A"), then attach more call legs to it. Every attached leg is a participant (also called a member).
- The host call is an answered
Callyou already control. Creating the conference puts that leg into the room as thehostparticipant. - A participant is one call leg in the room. You add participants
two ways: dial a brand-new outbound leg straight into the room
(
POST …/participants/), or attach a call leg you already have answered (POST …/actions/join/). - Each participant carries live state mirrored from the carrier:
is_muted,is_on_hold,is_speaking, plusjoined_at/left_at.
Lifecycle
init → in_progress → completed
status | Meaning |
|---|---|
init | Room row created; the create call has been issued but the carrier hasn't confirmed the room yet. |
in_progress | Room is live (confirmed by the conference.started webhook). |
completed | Room ended — by actions/end, by the host leaving when end_on_exit is set, or when it hits duration_minutes. Terminal. |
Almost every write checks the room is alive first. Acting on a
completed room returns 409 conference_already_ended.
The status transitions are driven by webhooks, not by your API call
returning — create returns a room in init, and it flips to
in_progress once the carrier fires conference.started. See
Webhooks.
The Conference resource
{
"id": "voepy_conf_01hz…",
"tenant_id": "0c8f…",
"name": "support-bridge-42",
"status": "in_progress",
"host_call_id": "voepy_call_01hz…",
"beep_enabled": "never",
"start_on_create": true,
"end_on_exit": true,
"max_participants": 10,
"duration_minutes": 720,
"started_at": "2026-05-12T14:32:18.000Z",
"ended_at": null,
"created_at": "2026-05-12T14:32:17.500Z",
"participants": [ /* ConferenceParticipant objects */ ]
}
| Field | Type | Notes |
|---|---|---|
id | voepy_conf_… | Opaque room id. |
name | string | Your label. Must be unique among your non-completed rooms — once a room ends you can reuse the name. |
status | enum | init, in_progress, completed. |
host_call_id | voepy_call_… | The leg the room was built on. null if that call has since been purged. |
beep_enabled | enum | never, on_enter, on_exit, always. When members hear an entry/exit beep. |
start_on_create | bool | Whether the room becomes active immediately on create vs. waiting for the second participant. |
end_on_exit | bool | Whether the room tears down when the host leg leaves. |
max_participants | int | 2–250. Adding past this returns 409 conference_full. |
duration_minutes | int | 1–2880 (max 48h). Hard cap on room lifetime. |
started_at / ended_at | timestamp | null until the room starts / ends. |
participants | array | Embedded participant list (see below). |
The ConferenceParticipant (member) resource
{
"id": "voepy_member_01hz…",
"conference_id": "voepy_conf_01hz…",
"call_id": "voepy_call_01hz…",
"role": "participant",
"joined_at": "2026-05-12T14:32:25.000Z",
"left_at": null,
"duration_seconds": 0,
"is_muted": false,
"is_on_hold": false,
"is_speaking": true
}
| Field | Type | Notes |
|---|---|---|
id | voepy_member_… | Member id — use this for GET / PATCH on a single participant. |
conference_id | voepy_conf_… | The room. |
call_id | voepy_call_… | The underlying call leg. Use this id to remove a participant. |
role | enum | host or participant. |
joined_at | timestamp | null while the dialled leg is still ringing; stamped on conference.member_joined. |
left_at | timestamp | null while attached; stamped on conference.member_left. |
duration_seconds | int | Time in the room. |
is_muted / is_on_hold / is_speaking | bool | Live state, mirrored from carrier webhooks. |
Two different ids identify a member:
voepy_member_…(GET / PATCH a participant) and the participant'svoepy_call_…(DELETE / remove). This mirrors the underlying call-control model — removal targets the call leg, per-member settings target the member row.
Create a conference
You need an answered (or bridged) call to host the room.
POST /v1/conferences/
Content-Type: application/json
Idempotency-Key: <uuid>
{
"name": "support-bridge-42",
"host_call_id": "voepy_call_01hz…",
"beep_enabled": "on_enter",
"start_on_create": true,
"end_on_exit": true,
"max_participants": 25,
"duration_minutes": 120
}
| Field | Type | Default | Notes |
|---|---|---|---|
name | string ≤120 | required | Unique among your active rooms. |
host_call_id | voepy_call_… | required | Must be one of your calls and currently answered or bridged. |
beep_enabled | enum | never | never, on_enter, on_exit, always. |
start_on_create | bool | true | Start the room immediately. |
end_on_exit | bool | true | Tear down when the host leg leaves. |
max_participants | int 2–250 | 10 | Room capacity. |
duration_minutes | int 1–2880 | 720 | Max room lifetime. |
Response: 201 with the Conference resource at status: "init".
The host call becomes the host participant.
Failure modes:
400 invalid_request—host_call_idisn't a well-formedvoepy_call_….404 not_found— that call doesn't belong to you.409 call_not_in_required_state— the host call isn't answered or bridged.
List conferences
GET /v1/conferences/
?status=in_progress
&name=support
&page=1
&page_size=50
| Query param | Notes |
|---|---|
status | Exact match: init, in_progress, completed. |
name | Case-insensitive substring match. |
page | 1-indexed, default 1. |
page_size | Default 20, max 200. |
Response is the standard page envelope:
{
"count": 37,
"next": "https://api.voepy.com/v1/conferences/?page=2",
"previous": null,
"results": [ /* Conference objects, newest first */ ]
}
Retrieve a conference
GET /v1/conferences/{id}/
Returns the full Conference resource including the embedded
participants array. 404 not_found if the id isn't yours.
Update settings
POST /v1/conferences/{id}/actions/update/
{
"duration_minutes": 240,
"beep_enabled": "always",
"end_on_exit": false,
"max_participants": 50
}
All four fields are optional; send only what you want to change.
| Field | Type | Notes |
|---|---|---|
duration_minutes | int 1–2880 | New room lifetime cap. |
beep_enabled | enum | never, on_enter, on_exit, always. |
end_on_exit | bool | Whether the host leaving ends the room. |
max_participants | int 2–250 | New capacity. |
These are wrapper-side preferences applied to subsequent participant
actions and reflected on the GET response. The updated Conference
resource is returned. 409 conference_already_ended on a finished
room.
End a conference
POST /v1/conferences/{id}/actions/end/
{}
Hangs up every leg and moves the room to completed. Idempotent in
spirit, but a second call against an already-ended room returns
409 conference_already_ended. The optional region field (see
Regions) is accepted.
Manage participants
Add a participant by dialing out
Dials a fresh outbound leg and attaches it to the room the moment it answers.
POST /v1/conferences/{id}/participants/
Content-Type: application/json
Idempotency-Key: <uuid>
{
"from": "+15125550100",
"to": "+15558675309",
"route_id": "voepy_route_01hz…",
"timeout_secs": 30,
"customer_reference": "ticket-991"
}
| Field | Type | Default | Notes |
|---|---|---|---|
from | string (E.164) | required | A DID you own (or set caller_id_forwarding). |
to | string (E.164) | required | Who to dial in. |
route_id | voepy_route_… | tenant default | Which connection to route on. |
timeout_secs | int 1–600 | carrier default | How long to ring. |
customer_reference | string ≤128 | — | Opaque to us; echoed on this leg's webhooks. |
caller_id_forwarding | bool | false | Dial from a number you don't own. Same rules as Calls — requires the per-tenant admin flag. |
Response: 201 with the new ConferenceParticipant. Its joined_at
is null until the leg answers and the carrier fires
conference.member_joined. The dial leg is a normal Call you can
inspect at GET /v1/calls/{id}.
Failure modes: 409 conference_full (room at max_participants),
409 conference_already_ended, 422 from_number_not_owned,
422 caller_id_forwarding_not_allowed.
Add an existing call leg
If you already have an answered call (e.g. an inbound caller you want to pull into the bridge), attach it rather than dialing a new leg.
POST /v1/conferences/{id}/actions/join/
{
"call_id": "voepy_call_01hz…",
"mute": false,
"hold": false
}
| Field | Type | Notes |
|---|---|---|
call_id | voepy_call_… | required. Must be yours and currently answered or bridged. |
mute | bool | Join muted. |
hold | bool | Join on hold. |
Returns the updated Conference. 409 call_not_in_required_state if
the call isn't answered/bridged; 409 conference_full;
404 not_found if the call isn't yours.
List participants
GET /v1/conferences/{id}/participants/list/
?muted=true
&on_hold=false
&include_left=false
&page=1
&page_size=50
| Query param | Notes |
|---|---|
muted | true / false. |
on_hold | true / false. |
include_left | true to include departed members; default false (active only). |
page / page_size | Standard paging; default page_size 20, max 200. |
Filters compose — ?muted=true&on_hold=false returns members muted but
not on hold. Returns the standard page envelope of
ConferenceParticipant objects, speakers first.
This local list is the authoritative snapshot: mid-call state
(is_muted, is_on_hold, is_speaking, joined_at, left_at) is
kept live from carrier webhooks. There is no whispering filter here —
supervisor role isn't persisted locally; passing ?whispering=…
returns 400 invalid_request pointing you at the upstream snapshot.
Upstream snapshot (incident tooling).
GET /v1/conferences/{id}/participants/upstream/bypasses the database and queries the carrier live, annotating each upstream row with the matchingvoepy_member_…id (ornull+"drift": truefor rows we don't recognize). It supportsmuted,on_hold,whisperingpluspage/page_size(1–100), and is page-based with ametablock rather than the standard envelope. Every call is a carrier round-trip — use it during incidents only; the local list is what you want in normal operation.
Get a single participant
GET /v1/conferences/{id}/participants/{member_id}/
{member_id} is a voepy_member_… id. Returns the
ConferenceParticipant. 404 member_not_found if that member isn't on
the room.
Update a participant
PATCH /v1/conferences/{id}/participants/{member_id}/
{
"end_on_exit": true,
"soft_end_on_exit": false,
"beep_enabled": "on_exit"
}
| Field | Type | Notes |
|---|---|---|
end_on_exit | bool | This member leaving ends the whole room. |
soft_end_on_exit | bool | Member leaving ends the room only once no un-held members remain. |
beep_enabled | enum | Per-member beep: never, on_enter, on_exit, always. |
All optional. Returns the updated member. 404 member_not_found if the
member has no upstream leg yet (still ringing) or isn't on the room.
Remove a participant
Removal targets the participant's call id, not the member id.
DELETE /v1/conferences/{id}/participants/{member_call_id}/
{member_call_id} is the participant's voepy_call_…. The leg is
removed from the room (the call itself isn't necessarily hung up).
Returns the ConferenceParticipant. 404 member_not_found if that
call isn't an active member.
Per-participant mute and hold
Mute, unmute, hold, and unhold target one or more participants by their
leg ids. Each takes a non-empty leg_ids array (1–64
voepy_leg_… ids). All return the updated Conference.
POST /v1/conferences/{id}/actions/mute/
{ "leg_ids": ["voepy_leg_01hz…", "voepy_leg_02hz…"] }
POST /v1/conferences/{id}/actions/unmute/
{ "leg_ids": ["voepy_leg_01hz…"] }
POST /v1/conferences/{id}/actions/hold/
{
"leg_ids": ["voepy_leg_01hz…"],
"audio_url": "https://cdn.example.com/hold-music.mp3"
}
POST /v1/conferences/{id}/actions/unhold/
{ "leg_ids": ["voepy_leg_01hz…"] }
| Verb | Extra fields |
|---|---|
mute / unmute | leg_ids only. |
hold | leg_ids, optional audio_url (hold music played to the held legs). |
unhold | leg_ids only. |
A leg that isn't an active member of the room returns
404 member_not_found. The resulting is_muted / is_on_hold state
flows back via webhooks and is reflected on the next
participant list.
In-conference media
Play audio
Stream an audio file into the room (or to specific legs).
POST /v1/conferences/{id}/actions/play/
{
"url": "https://cdn.example.com/announcement.mp3",
"loop": 1,
"leg_ids": ["voepy_leg_01hz…"]
}
| Field | Type | Notes |
|---|---|---|
url | URL | required. The audio file to play. |
loop | int 1–100 | Repeat count. |
leg_ids | array of voepy_leg_… | Omit to play to the whole room; provide to target specific members. |
Speak (text-to-speech)
POST /v1/conferences/{id}/actions/speak/
{
"payload": "The meeting will end in five minutes.",
"voice": "Polly.Joanna",
"language": "en-US",
"payload_type": "text"
}
| Field | Type | Notes |
|---|---|---|
payload | string ≤2000 | required. Text (or SSML) to speak. |
voice | string | required. TTS voice id, e.g. Polly.Joanna. |
language | string | BCP-47 tag, e.g. en-US. |
payload_type | enum | text (default) or ssml. |
leg_ids | array of voepy_leg_… | Omit for the whole room; provide to target members. |
Stop audio / speak
POST /v1/conferences/{id}/actions/stop/
{ "leg_ids": ["voepy_leg_01hz…"] }
Stops in-progress playback and speech. Omit leg_ids to stop for the
whole room; provide it to stop only on specific legs. Returns the
Conference.
Recording
Conference recording captures the mixed room audio. Start, stop, pause, and resume independently of any per-leg call recording.
POST /v1/conferences/{id}/actions/record_start/
{
"format": "mp3",
"channels": "single",
"play_beep": true
}
| Field | Type | Default | Notes |
|---|---|---|---|
format | enum | required | mp3 or wav. |
channels | enum | single | single (mixed) or dual. |
play_beep | bool | — | Audible record-start beep. |
POST /v1/conferences/{id}/actions/record_stop/ { }
POST /v1/conferences/{id}/actions/record_pause/ { }
POST /v1/conferences/{id}/actions/record_resume/ { }
All four return the Conference. When the recording is ready you'll
get a recording.completed webhook; the resulting Recording carries
the conference, and its call_id resolves to the host call. Fetch and
manage the file through the recordings API — see
Recordings.
Supervise (whisper / monitor / barge)
Attach a supervisor to the room and set their relationship to the rest of the participants — the standard call-center coaching primitives.
POST /v1/conferences/{id}/actions/supervise/
{
"leg_id": "voepy_leg_supervisor…",
"role": "whisper",
"whisper_leg_ids": ["voepy_leg_agent…"]
}
| Field | Type | Notes |
|---|---|---|
leg_id | voepy_leg_… | required. The supervisor's leg. Must be an active member. |
role | enum | required. monitor, whisper, barge, or none. |
whisper_leg_ids | array of voepy_leg_… | Required when role is whisper — the legs the supervisor can be heard by. Ignored for other roles. |
| Role | What the supervisor can do |
|---|---|
monitor | Listen only. No one in the room hears the supervisor. |
whisper | Speak privately to the legs in whisper_leg_ids (coaching) while hearing the whole room; other members don't hear the supervisor. |
barge | Full two-way — the supervisor hears and is heard by everyone. |
none | Clear the supervisor role; revert to a normal participant. |
Returns the Conference. Omitting whisper_leg_ids with
role: "whisper" is a 400 invalid_request.
Regions
Hold, mute, play, speak, record, join, supervise, and end all accept an
optional region field. Valid values: Australia, Europe,
Middle East, US. Leave it off unless you have a specific routing
reason to pin a region.
Events {#events}
Conference activity is delivered through your webhook subscriptions using wrapper-public event names:
| Event | Fires when | Key payload fields |
|---|---|---|
conference.started | Room goes live | conference_id, name, host_call_id |
conference.ended | Room completes | conference_id, ended_at, duration_seconds |
conference.member_joined | A leg attaches | conference_id, member_id, call_id, role, joined_at |
conference.member_left | A leg detaches | conference_id, member_id, call_id, role, left_at |
conference.member_muted | A member is muted | conference_id, member_id, call_id |
conference.member_unmuted | A member is unmuted | conference_id, member_id, call_id |
conference.member_speaking_started | A member starts talking | conference_id, member_id, call_id |
conference.member_speaking_ended | A member stops talking | conference_id, member_id, call_id |
recording.completed | A conference recording is ready | recording fields (see Recordings) |
Subscribe to these (or ["*"]) the same way as call events. See
Webhooks for subscription setup, signature
verification, and retry behavior.
Errors specific to conferences
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_request | Malformed body, bad id format, or an unsupported filter (e.g. whispering on the local list). |
| 404 | not_found | Conference or host/join call doesn't belong to you. |
| 404 | member_not_found | The targeted member, leg, or call isn't an active participant. |
| 409 | conference_full | Room is at max_participants. |
| 409 | conference_already_ended | Action attempted on a completed room. |
| 409 | call_not_in_required_state | Host/join call isn't answered or bridged. |
| 422 | from_number_not_owned | Dial-in from isn't a DID you own and forwarding isn't enabled. |
| 422 | caller_id_forwarding_not_allowed | caller_id_forwarding: true without the admin opt-in. |
| 502 | upstream_error | Carrier-side failure. Often retriable; check the message. |
Every error uses the standard envelope:
{ "error": { "code": "conference_full", "message": "…" } }
Full reference: Error reference.
Next
- Calls — place and answer the legs you bridge into a room.
- Recordings — fetch and manage conference recordings.
- Webhooks — subscribe to the
conference.*events that drive your room logic. - Error reference — every code the API can return.
