Manage Multiple Calls
The Telnyx WebRTC JS SDK supports multiple simultaneous calls within a singleTelnyxRTC client session. This guide covers concurrent call management, per-call media elements, call waiting, hold/transfer patterns, and the warnings the SDK emits when multiple calls are active.
The SDK does not recommend having multiple active calls simultaneously and cannot guarantee stable behavior in all scenarios. The supported pattern is accepting and holding an inbound call while finishing an active one — the SDK handles this well. Running two fully active calls at the same time (both with bidirectional audio) is not recommended and may produce unpredictable media or signaling behavior.
Overview
A singleTelnyxRTC instance connected to rtc.telnyx.com can have multiple active calls at the same time. Each call has its own:
- Call ID (
call.id) — unique per call leg - Direction (
call.direction) —inboundoroutbound - State (
call.state) —ringing,trying,active,held,hangup,destroyed - PeerConnection — independent
RTCPeerConnectionper call - Media element —
remoteElement/localElement(see Per-call media elements)
Per-call media elements
Starting with SDK v2.27.4, you can assign a distinctremoteElement (and localElement) per call. This is essential for concurrent calls — without it, all calls share the same audio element and the SDK emits a SHARED_REMOTE_ELEMENT_OVERWRITE warning when a second call overwrites the first call’s stream.
Why per-call elements matter
| Scenario | Shared element | Per-call element |
|---|---|---|
| Two active calls | Second call overwrites first call’s audio; SHARED_REMOTE_ELEMENT_OVERWRITE warning | Each call plays through its own <audio> element independently |
| Hang up one call | Element is detached; remaining call may lose audio | Only the hung-up call’s element is detached; other call’s audio continues |
| Call waiting UI | Can’t play ringback for second call while first is active | Ringback plays on second call’s element; first call continues on its element |
Outbound calls
PassremoteElement at call creation:
Inbound calls
PassremoteElement when answering:
Backward compatibility
Omitting the per-call params keeps the session-levelclient.remoteElement as the fallback default. This is backward compatible with existing single-call integrations — no changes are needed for apps that handle only one call at a time.
Call waiting
When a second inbound call arrives while a call is already active, the SDK emits aMULTIPLE_ACTIVE_CALLS_DETECTED warning (33010). This is diagnostic only — the SDK does not block the second call.
Accept the second call
Reject the second call
Hold and switch between calls
Usehold() and unhold() to switch between concurrent calls:
hold() sends a re-INVITE to the remote party to pause media. The call stays in the held state and can be resumed with unhold().Warnings for multiple calls
| Code | Name | When | Action |
|---|---|---|---|
33010 | MULTIPLE_ACTIVE_CALLS_DETECTED | A new call started while another is active | Verify this is intentional (call waiting/transfer); otherwise hang up the previous call |
33011 | SHARED_REMOTE_ELEMENT_OVERWRITE | Two calls sharing the same remoteElement | Assign a distinct remoteElement per call (see above) |
33007 | DUPLICATE_INBOUND_ANSWER | Same callID answered twice | Since v2.27.4, this is scoped per callID — a different callID proceeds normally; same callID emits a warning without hangup |
Transfer
Blind transfer and attended transfer work with multiple calls. To perform an attended transfer:Server-dialed consult leg (auto-answered second inbound call)
Some attended-transfer and consult flows are driven from the server: while the agent is on their customer call, Call Control dials the agent’s own credential as a second leg and the client auto-answers it. Because both legs are inbound calls on the same single registration, this depends on the SDK allowing a secondanswer() for a distinct call ID.
In v2.27.0–v2.27.3 this second
answer() was silently ignored: the
duplicate-answer guard keyed on any other active inbound call, so the second
leg stayed ringing with only a DUPLICATE_INBOUND_ANSWER (33007) warning
and no error or state change (webrtc#726).
v2.27.4 fixes this — the guard is scoped per call ID, so answering a
genuinely distinct second inbound call proceeds normally. The previous
call.options.attach = true workaround is no longer needed.DUPLICATE_INBOUND_ANSWER (33007) warning now fires only when the same
call ID is answered twice (for example a duplicate WebSocket registration of the
same leg); it is warning-only and never tears down established media.
Best practices for concurrent calls
- One active call at a time — the SDK does not recommend having two fully active calls simultaneously. Use hold to manage the active call and accept/hold an inbound call while finishing the active one.
-
Assign distinct
remoteElementper call — use separate<audio>or<video>elements for each call to avoidSHARED_REMOTE_ELEMENT_OVERWRITEwarnings and ensure independent playout lifecycles. -
Track calls by ID — use
call.idas the key in your application state. The call ID may change after reconnection (see recoveredCallId). -
Clean up on
destroyed— remove call references from your state whencall.state === 'destroyed'to prevent memory leaks. -
Use
hold()before answering — when accepting a second call, hold the first call first to avoid overlapping audio. -
One
TelnyxRTCinstance — keep a single client instance per tab/session. Multiple instances with the same credential causeDUPLICATE_INBOUND_ANSWERwarnings.
See Also
- Framework Integration — Per-call
remoteElementin React, Vue, Angular - Error Handling — Warning codes for multi-call scenarios
- Handle Reconnection — How calls survive reconnection
- IClientOptions — Client configuration
- ICallOptions — Call options including
remoteElement