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

# WebRTC call control

> Control a Realtime API WebRTC call from your server: hang it up, or attach a sideband WebSocket to watch its events, update the session and answer tool calls.

<Note>
Call control is in beta, its behavior may still change.
</Note>

Call control lets your server act on a [WebRTC](https://docs.inworld.ai/realtime/connect/webrtc.md) call that a browser or mobile client started. It works like OpenAI's [server-side controls](https://developers.openai.com/api/docs/guides/voice-server-controls) (also called a sideband connection) and has two parts:

- **Hang up:** `POST /v1/realtime/calls/{call_id}/hangup` ends the call.
- **Sideband:** a WebSocket at `wss://api.inworld.ai/v1/realtime?call_id={call_id}` receives the call's events and sends client events to it.

Use it when the client should only carry audio, and your server should own the rest: instructions, tools that need your secrets or database, and when the call ends. The API key stays on your server.

Call control works only with WebRTC calls. A [WebSocket](https://docs.inworld.ai/realtime/connect/websocket.md) session cannot be attached or hung up. Whoever opened it controls it, and closes that WebSocket to end it.

## Get the call ID

`POST /v1/realtime/calls` returns `201 Created` with the call ID in two places:

- the `Location` header, `/v1/realtime/calls/<call_id>`, for every request content type;
- the `id` field of the JSON response, when the request is `application/json`.

The call ID is a UUID. Have your client send it to your server after the call starts. Both call-control endpoints take this ID.

<Warning>
Do not pass a call ID as the `key` of a WebSocket session (`/api/v1/realtime/session?key=<call_id>`). That opens a separate, empty session, not the call. Use the sideband to reach a WebRTC call.
</Warning>

## Hang up a call

Send `POST https://api.inworld.ai/v1/realtime/calls/{call_id}/hangup` with your API key. There is no request body.

```bash
curl -X POST "https://api.inworld.ai/v1/realtime/calls/$CALL_ID/hangup" \
  -H "Authorization: Basic $INWORLD_API_KEY"
```

A `200` means the call has started to end. See [Hang Up Call](https://docs.inworld.ai/api-reference/realtimeAPI/realtime/realtime-webrtc.md#hang-up-call) for every status and what Inworld closes.

## Attach a sideband

Open a WebSocket to `wss://api.inworld.ai/v1/realtime?call_id={call_id}` from your server, with the header `Authorization: Basic <INWORLD_API_KEY>`. See [Sideband WebSocket](https://docs.inworld.ai/api-reference/realtimeAPI/realtime/realtime-webrtc.md#sideband-websocket) for the other authentication form, handshake errors and limits.

### What the sideband receives

The sideband receives the same server events as the call's data channel, in the same order, including `error` events. Nothing is sent when the sideband attaches: there is no `session.created`. Response audio travels on the media track. [`response.output_audio.delta`](https://docs.inworld.ai/api-reference/realtimeAPI/realtime/realtime-webrtc.md#responseoutput_audiodelta) reaches the sideband only as an alignment event with an empty `delta`, when `providerData.tts.timestamp_type` is set. When back-channel is enabled, [`response.backchannel.audio.delta`](https://docs.inworld.ai/api-reference/realtimeAPI/realtime/realtime-webrtc.md#responsebackchannelaudiodelta) events, which carry audio, are sent too.

### What the sideband can send

Client events sent on the sideband apply to the call exactly as if the client had sent them on the data channel. For example, send `session.update`, `conversation.item.create` (including `function_call_output` items), `response.create` and `response.cancel`. See the [WebRTC API reference](https://docs.inworld.ai/api-reference/realtimeAPI/realtime/realtime-webrtc.md#client-events) for each event.

The sideband takes no audio input. Inworld refuses `input_audio_buffer.append` with an `error` event.

### Limits

A sideband does not count toward your [concurrent session limit](https://docs.inworld.ai/portal/billing.md#realtime-api-features-and-limits-by-plan) and is not billed separately. For the attach rate limit and when Inworld closes a slow sideband, see [Sideband WebSocket](https://docs.inworld.ai/api-reference/realtimeAPI/realtime/realtime-webrtc.md#sideband-websocket).

## Answer tool calls from your server

A server that answers tool calls over the sideband should control when the model continues:

1. Set `providerData.auto_tool_response` to `false`. Your server can do this itself with `session.update` on the sideband.
2. When you receive `response.function_call_arguments.done`, run the tool and add a `function_call_output` item with the tool call's `call_id` from that event (not the WebRTC call ID).
3. Send `response.create` once all the outputs for that response are in.

If you keep the default (`auto_tool_response: true`), send only the `function_call_output` item, and Inworld continues on its own. Do not combine the default with your own `response.create`: Inworld then starts an extra, empty response that is cancelled first. See [Control tool continuation](https://docs.inworld.ai/realtime/usage/tool-calling.md#control-tool-continuation).

Answer each tool on one side only. The client's data channel receives the same tool calls, so a client that also answers them sends a second output.

## Example

This Node.js server attaches a sideband to a call, sets the instructions and a tool, answers tool calls, and hangs up after five minutes. It uses the `ws` library. Pass the call ID that your client received from `POST /v1/realtime/calls`.

```javascript
import WebSocket from 'ws';

const API_KEY = process.env.INWORLD_API_KEY;
const callId = process.argv[2];

const sideband = new WebSocket(
  `wss://api.inworld.ai/v1/realtime?call_id=${encodeURIComponent(callId)}`,
  { headers: { Authorization: `Basic ${API_KEY}` } }
);

const send = (event) => sideband.send(JSON.stringify(event));

async function getWeather({ location }) {
  return { location, forecast: 'sunny', temperature_c: 21 };
}

async function hangUp() {
  const res = await fetch(
    `https://api.inworld.ai/v1/realtime/calls/${encodeURIComponent(callId)}/hangup`,
    { method: 'POST', headers: { Authorization: `Basic ${API_KEY}` } }
  );
  if (res.status === 404) console.log('The call has already ended');
  else if (!res.ok) console.error('Hangup failed:', res.status);
}

let hangUpTimer;

sideband.on('open', () => {
  send({
    type: 'session.update',
    session: {
      type: 'realtime',
      instructions: 'You are a concise concierge. Use get_weather for weather questions.',
      tools: [{
        type: 'function',
        name: 'get_weather',
        description: 'Fetch the weather for a location',
        parameters: {
          type: 'object',
          properties: { location: { type: 'string' } },
          required: ['location']
        }
      }],
      providerData: { auto_tool_response: false }
    }
  });
  hangUpTimer = setTimeout(hangUp, 5 * 60 * 1000);
});

sideband.on('message', async (data) => {
  const event = JSON.parse(data.toString());

  if (event.type === 'error') {
    console.error(event.error?.code, event.error?.message);
    return;
  }
  if (event.type !== 'response.function_call_arguments.done' || event.name !== 'get_weather') return;

  const result = await getWeather(JSON.parse(event.arguments));
  send({
    type: 'conversation.item.create',
    item: { type: 'function_call_output', call_id: event.call_id, output: JSON.stringify(result) }
  });
  send({ type: 'response.create' });
});

sideband.on('close', (code) => {
  clearTimeout(hangUpTimer);
  console.log('Sideband closed:', code);
});
```

This example sends `response.create` after each tool output. If a response can call several tools, send it once, after the last output.

## When a call ends

Closing a sideband never ends the call. Use [hangup](#hang-up-a-call) to end it. When the call ends, Inworld closes every attached sideband. See [Sideband WebSocket](https://docs.inworld.ai/api-reference/realtimeAPI/realtime/realtime-webrtc.md#sideband-websocket) for how it closes.

## Next steps

<CardGroup cols={2}>
  <Card title="WebRTC API reference" icon="code" href="https://docs.inworld.ai/api-reference/realtimeAPI/realtime/realtime-webrtc.md#call-control">
    Hangup and sideband contracts, and every client and server event.
  </Card>

  <Card title="Tool calling" icon="plug" href="https://docs.inworld.ai/realtime/usage/tool-calling.md">
    Register tools, handle tool calls and return results.
  </Card>
</CardGroup>
