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

# WebSocket signaling

> WebSocket signaling protocol for SDP exchange, ICE candidates, and session management.

The signaling channel is a WebSocket connection used to exchange SDP offers/answers and ICE candidates between your client and the WebRTC Gateway.

## Endpoint

```
wss://webrtc-gateway.us-1.platform.polyai.app/api/v1/webrtc/signal
```

## Message format

All messages are JSON objects with a common top-level structure.

<ResponseField name="type" type="string" required>
  Message type. One of `offer`, `answer`, `ice-candidate`, `error`, `close`.
</ResponseField>

<ResponseField name="sessionId" type="string" required>
  Session identifier. Send an empty string (`""`) when creating a new session with an offer.
</ResponseField>

<ResponseField name="data" type="object">
  Message-specific payload. Structure depends on the message type.
</ResponseField>

<ResponseField name="authToken" type="string" required>
  Authentication token. Required in every `offer` message, including studio draft and preview calls. Offers without a valid `authToken` are rejected with an `UNAUTHORIZED` error (`Auth token required`). Setting `agentVersionOverride`, `accountId`, or `projectId` does not exempt a call from authentication.
</ResponseField>

<ResponseField name="mode" type="string">
  Agent mode for the session. Defaults to `end-to-end` when omitted.

  | Value         | Description                                                                      |
  | ------------- | -------------------------------------------------------------------------------- |
  | `end-to-end`  | End-to-end mode. A single speech-to-speech model handles audio input and output. |
  | `traditional` | Traditional mode. Audio cascades through separate ASR, LLM, and TTS stages.      |

  Unknown values are rejected with an `INVALID_ARGUMENT` error. Legacy values (`agent`, `agent_v1`, `agent_v2`, `cascaded`, `normal`, `realtime`) are still accepted but resolve to a canonical value and may be removed in a future release. Update integrations to use the canonical names.
</ResponseField>

<Note>
  **Echo mode is restricted to debug environments.** Echo mode (`mode: "echo"`) loops your audio back to the client and is intended for connectivity testing. Production gateways reject `echo` offers with a `FORBIDDEN` error before any session is created. Use `agent` mode (the default) for normal voice agent traffic.
</Note>

<ResponseField name="callSid" type="string">
  Unique call identifier (camelCase – this is distinct from the [Outbound Calling API](/api-reference/outbound/endpoint/trigger-call)'s `call_sid` field).
</ResponseField>

<ResponseField name="caller" type="string">
  Calling number.
</ResponseField>

<ResponseField name="callee" type="string">
  Called number.
</ResponseField>

<ResponseField name="accountId" type="string">
  Account identifier.
</ResponseField>

<ResponseField name="projectId" type="string">
  Project identifier.
</ResponseField>

<ResponseField name="variantId" type="string">
  Optional variant override.
</ResponseField>

<ResponseField name="agentVersionOverride" type="object">
  Optional pinning of a specific agent build. Both fields are required when set:

  * `artifactVersion` (string)
  * `lambdaDeploymentVersion` (string)

  Setting `agentVersionOverride` does not bypass authentication. A valid `authToken` is still required.
</ResponseField>

## Operations

### Send offer

**Direction: client to server**

Starts a new session. Send with an empty `sessionId` and include your `authToken`.

The `data` field contains the SDP offer:

<ResponseField name="data.type" type="string" required>
  Must be `"offer"`.
</ResponseField>

<ResponseField name="data.sdp" type="string" required>
  Full SDP string from your local peer connection.
</ResponseField>

```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "offer",
  "sessionId": "",
  "data": {
    "type": "offer",
    "sdp": "v=0\r\no=- 4611731400430051336 2 IN IP4 127.0.0.1..."
  },
  "authToken": "your-auth-token",
  "callSid": "call-unique-id",
  "caller": "+14155551234",
  "callee": "+14155555678"
}
```

### Receive answer

**Direction: server to client**

Sent by the server in response to a valid offer. Contains the SDP answer and the assigned `sessionId`. Store the `sessionId` and use it for all subsequent messages.

The `data` field contains the SDP answer:

<ResponseField name="data.type" type="string" required>
  Must be `"answer"`.
</ResponseField>

<ResponseField name="data.sdp" type="string" required>
  Full SDP string from the server.
</ResponseField>

```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "answer",
  "sessionId": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "type": "answer",
    "sdp": "v=0\r\no=- 4611731400430051336 2 IN IP4 192.168.1.1..."
  }
}
```

### Exchange ICE candidates

**Direction: bidirectional**

Sent by both client and server to exchange network connectivity candidates. Continue exchanging until the WebRTC connection is established.

The `data` field contains the ICE candidate:

<ResponseField name="data.candidate" type="string" required>
  ICE candidate string.
</ResponseField>

<ResponseField name="data.sdpMid" type="string" required>
  Media stream identification tag.
</ResponseField>

<ResponseField name="data.sdpMLineIndex" type="integer" required>
  Zero-based index of the media description in the SDP.
</ResponseField>

```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "ice-candidate",
  "sessionId": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "candidate": "candidate:1 1 UDP 2130706431 192.168.1.1 54321 typ host",
    "sdpMid": "0",
    "sdpMLineIndex": 0
  }
}
```

### Close

**Direction: client to server**

Terminates the session gracefully. Send when you want to end the call.

```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "close",
  "sessionId": "550e8400-e29b-41d4-a716-446655440000"
}
```

### Error

**Direction: server to client**

Sent when the server encounters an error during the session.

The `data` field contains the error details:

<ResponseField name="data.code" type="string" required>
  Error code identifying the failure type.
</ResponseField>

<ResponseField name="data.message" type="string" required>
  Human-readable error description.
</ResponseField>

```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "error",
  "sessionId": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "code": "UNAUTHORIZED",
    "message": "Invalid authentication token"
  }
}
```

#### Error codes

| Code                   | Description                                                         |
| ---------------------- | ------------------------------------------------------------------- |
| `UNAUTHORIZED`         | Invalid or missing authentication token                             |
| `FORBIDDEN`            | The requested action is not permitted on this gateway               |
| `INVALID_ARGUMENT`     | Request field has an invalid value (for example, an unknown `mode`) |
| `INVALID_MESSAGE`      | Malformed or unsupported message format                             |
| `HANDLER_ERROR`        | Error processing the signaling message                              |
| `MEDIA_BRIDGE_FAILURE` | Failed to establish the media connection                            |
| `AGENT_FAILURE`        | Error connecting to the PolyAI agent                                |

## Connection flow

<Steps>
  <Step title="Open WebSocket">
    Connect to the signaling endpoint using a WebSocket client.
  </Step>

  <Step title="Send offer">
    Create a local `RTCPeerConnection`, add your microphone track, generate an SDP offer, and send it with your `authToken`.
  </Step>

  <Step title="Receive answer">
    The server responds with an SDP answer and a `sessionId`. Set the remote description on your peer connection.
  </Step>

  <Step title="Exchange ICE candidates">
    Forward ICE candidates from your `onicecandidate` handler. Add incoming candidates from the server to your peer connection.
  </Step>

  <Step title="Audio flows">
    Once ICE negotiation completes, bidirectional audio streams between the browser and the PolyAI agent.
  </Step>

  <Step title="Close session">
    Send a `close` message when the conversation ends.
  </Step>
</Steps>
