> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Manage Multiple Calls

> How to handle concurrent calls, call waiting, and per-call media element management with the Telnyx WebRTC JS SDK.

# Manage Multiple Calls

The Telnyx WebRTC JS SDK supports multiple simultaneous calls within a single `TelnyxRTC` 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.

<Callout type="warning">
  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.
</Callout>

***

## Overview

A single `TelnyxRTC` 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`) — `inbound` or `outbound`
* **State** (`call.state`) — `ringing`, `trying`, `active`, `held`, `hangup`, `destroyed`
* **PeerConnection** — independent `RTCPeerConnection` per call
* **Media element** — `remoteElement` / `localElement` (see [Per-call media elements](#per-call-media-elements))

```javascript theme={null}
const client = new TelnyxRTC({ login_token: jwt });
client.connect();

// client.calls is an array of all call objects
client.on('telnyx.notification', (notification) => {
  if (notification.type === 'callUpdate') {
    console.log(`Active calls: ${client.calls.length}`);
    client.calls.forEach(call => {
      console.log(`  ${call.id}: ${call.direction} ${call.state}`);
    });
  }
});
```

***

## Per-call media elements

Starting with SDK v2.27.4, you can assign a distinct `remoteElement` (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

Pass `remoteElement` at call creation:

```javascript theme={null}
// First call
const callA = client.newCall({
  destinationNumber: '+12345678900',
  remoteElement: document.getElementById('remoteMediaA'),
});

// Second call (while first is still active)
const callB = client.newCall({
  destinationNumber: '+19876543210',
  remoteElement: document.getElementById('remoteMediaB'),
});
```

### Inbound calls

Pass `remoteElement` when answering:

```javascript theme={null}
client.on('telnyx.notification', (notification) => {
  if (notification.type === 'callUpdate') {
    const call = notification.call;

    if (call.state === 'ringing' && call.direction === 'inbound') {
      // Answer with a specific remote element
      call.answer({
        remoteElement: document.getElementById('remoteMediaB'),
      });
    }
  }
});
```

### Backward compatibility

Omitting the per-call params keeps the session-level `client.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.

```javascript theme={null}
// Session-level default — used when no per-call element is specified
client.remoteElement = 'remoteMedia';

// This call uses the session-level default
const call = client.newCall({ destinationNumber: '+12345678900' });
```

***

## Call waiting

When a second inbound call arrives while a call is already active, the SDK emits a `MULTIPLE_ACTIVE_CALLS_DETECTED` warning (33010). This is diagnostic only — the SDK does not block the second call.

```javascript theme={null}
import { SwEvent, TELNYX_WARNING_CODES } from '@telnyx/webrtc';

client.on(SwEvent.Warning, ({ warning, callId, sessionId }) => {
  if (warning.code === TELNYX_WARNING_CODES.MULTIPLE_ACTIVE_CALLS_DETECTED) {
    // Show call-waiting UI
    showCallWaitingPrompt(callId);
  }
});
```

### Accept the second call

```javascript theme={null}
// Put the first call on hold
const firstCall = client.calls[0];
firstCall.hold();

// Answer the second call with its own remote element
const secondCall = client.calls[1];
secondCall.answer({
  remoteElement: document.getElementById('remoteMediaB'),
});
```

### Reject the second call

```javascript theme={null}
const secondCall = client.calls[1];
secondCall.hangup();
```

***

## Hold and switch between calls

Use `hold()` and `unhold()` to switch between concurrent calls:

```javascript theme={null}
async function switchToCall(targetCall) {
  // Hold all other calls
  client.calls.forEach(call => {
    if (call !== targetCall && call.state === 'active') {
      call.hold();
    }
  });

  // Unhold the target call
  await targetCall.unhold();
}
```

<Callout type="info">
  `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()`.
</Callout>

***

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

See [Error Handling](/development/webrtc/js-sdk/how-to/error-handling) for the full warning reference.

***

## Transfer

Blind transfer and attended transfer work with multiple calls. To perform an attended transfer:

```javascript theme={null}
// Call A is active
const callA = client.calls[0];

// Make call B to the transfer target
const callB = client.newCall({
  destinationNumber: '+19876543210',
  remoteElement: document.getElementById('remoteMediaB'),
});

// Once call B is active, transfer call A to call B
callB.on('telnyx.notification', (notification) => {
  if (notification.call.state === 'active') {
    callA.transfer(callB);
  }
});
```

### 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 second `answer()` for a *distinct* call ID.

<Callout type="warning">
  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](https://github.com/team-telnyx/webrtc/issues/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.
</Callout>

Auto-answer the server-dialed leg when it rings (identify it however your
backend marks it — e.g. a custom SIP header):

```javascript theme={null}
client.on('telnyx.notification', (notification) => {
  if (notification.type !== 'callUpdate') return;
  const call = notification.call;

  // Distinct second inbound leg dialed by your server (e.g. a consult leg
  // tagged with a custom header). In v2.27.4 this answer() is honored even
  // while the first call is still active/held.
  if (call.state === 'ringing' && isConsultLeg(call)) {
    client.calls
      .filter((c) => c !== call && c.state === 'active')
      .forEach((c) => c.hold());

    call.answer({ remoteElement: document.getElementById('remoteMediaB') });
  }
});
```

A `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

1. **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.

2. **Assign distinct `remoteElement` per call** — use separate `<audio>` or `<video>` elements for each call to avoid `SHARED_REMOTE_ELEMENT_OVERWRITE` warnings and ensure independent playout lifecycles.

3. **Track calls by ID** — use `call.id` as the key in your application state. The call ID may change after reconnection (see [recoveredCallId](/development/webrtc/js-sdk/how-to/handle-reconnection#recoveredcallid)).

4. **Clean up on `destroyed`** — remove call references from your state when `call.state === 'destroyed'` to prevent memory leaks.

5. **Use `hold()` before answering** — when accepting a second call, hold the first call first to avoid overlapping audio.

6. **One `TelnyxRTC` instance** — keep a single client instance per tab/session. Multiple instances with the same credential cause `DUPLICATE_INBOUND_ANSWER` warnings.

***

## See Also

* [Framework Integration](/development/webrtc/js-sdk/how-to/integrate-with-frameworks) — Per-call `remoteElement` in React, Vue, Angular
* [Error Handling](/development/webrtc/js-sdk/how-to/error-handling) — Warning codes for multi-call scenarios
* [Handle Reconnection](/development/webrtc/js-sdk/how-to/handle-reconnection) — How calls survive reconnection
* [IClientOptions](/development/webrtc/js-sdk/interfaces/iclientoptions) — Client configuration
* [ICallOptions](/development/webrtc/js-sdk/interfaces/icalloptions) — Call options including `remoteElement`
