> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.telekesher.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.telekesher.dev/_mcp/server.

# WebSocket events

WebSocket events are lifecycle webhooks and control-plane callbacks for an audio relay between a session and your `wss://` backend — started with the `websocket/start` command, or born with a standalone `type: "websocket"` session that has no phone leg. They report relay connection, termination, failure, and backend text frames. A session can have at most one relay active at a time; a second `websocket/start` while one is already running returns `409`.

All webhook deliveries are signed. See [Verifying signatures](/webhooks/webhook-signatures) for how to validate event authenticity.

## websocket.connected

Fired when your backend accepts the relay's WebSocket connection and audio begins flowing. For a standalone WebSocket session, this is also when the session advances from connecting to answered.

**Payload schema**

| Field          | Type   | Description                                                                                                        |
| -------------- | ------ | ------------------------------------------------------------------------------------------------------------------ |
| `event`        | string | Always `"websocket.connected"`. **Required**                                                                       |
| `session_uuid` | string | The session the relay is attached to. **Required**                                                                 |
| `timestamp`    | string | RFC 3339 / ISO 8601 UTC timestamp at which the event was emitted (e.g. `"2025-06-06T08:53:20.000Z"`). **Required** |

```json
{
  "event": "websocket.connected",
  "session_uuid": "acW68-f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "timestamp": "2025-06-06T08:53:20.000Z"
}
```

## websocket.disconnected

Fired when the relay reports a server-initiated close or error. A deliberate `websocket/stop` does not emit this event. On an unexpected close of a standalone WebSocket session, the session also ends, so a [`call.ended`](/webhooks/call-events#callended) follows.

**Payload schema**

| Field          | Type    | Description                                                                                                                                                      |
| -------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event`        | string  | Always `"websocket.disconnected"`. **Required**                                                                                                                  |
| `session_uuid` | string  | The session the relay was attached to. **Required**                                                                                                              |
| `code`         | integer | [WebSocket close code](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code) (e.g. `1000` for a normal close), or `0` when unavailable. **Required** |
| `reason`       | string  | Human-readable close reason (e.g. `"bye"`), or an empty string when unavailable. **Required**                                                                    |
| `timestamp`    | string  | RFC 3339 / ISO 8601 UTC timestamp at which the event was emitted. **Required**                                                                                   |

```json
{
  "event": "websocket.disconnected",
  "session_uuid": "acW68-f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "code": 1000,
  "reason": "bye",
  "timestamp": "2025-06-06T08:55:01.000Z"
}
```

## websocket.failed

Fired when the relay cannot be established — your backend is unreachable, rejects the handshake, or the connection drops before it is usable. On a standalone WebSocket session the session ends, so a [`call.ended`](/webhooks/call-events#callended) follows.

**Payload schema**

| Field          | Type    | Description                                                                                                   |
| -------------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| `event`        | string  | Always `"websocket.failed"`. **Required**                                                                     |
| `session_uuid` | string  | The session the relay was attached to. **Required**                                                           |
| `http_status`  | integer | HTTP status returned by your backend during the handshake (e.g. `401`), or `0` when unavailable. **Required** |
| `error`        | string  | Failure category. One of `"handshake_failed"` or `"connection_failed"`. **Required**                          |
| `timestamp`    | string  | RFC 3339 / ISO 8601 UTC timestamp at which the event was emitted. **Required**                                |

```json
{
  "event": "websocket.failed",
  "session_uuid": "acW68-f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "http_status": 401,
  "error": "handshake_failed",
  "timestamp": "2025-06-06T08:53:21.000Z"
}
```

## websocket.message\_received

Fired when your backend sends a text frame back over the relay. Use it for out-of-band control or metadata from your backend. To send a final text frame before closing the relay, use `websocket/stop` with `final_text`.

**Payload schema**

| Field          | Type   | Description                                                                    |
| -------------- | ------ | ------------------------------------------------------------------------------ |
| `event`        | string | Always `"websocket.message_received"`. **Required**                            |
| `session_uuid` | string | The session the relay is attached to. **Required**                             |
| `data`         | string | The text frame sent by your backend (size-capped). **Required**                |
| `timestamp`    | string | RFC 3339 / ISO 8601 UTC timestamp at which the event was emitted. **Required** |

```json
{
  "event": "websocket.message_received",
  "session_uuid": "acW68-f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "data": "{\"intent\":\"transfer\"}",
  "timestamp": "2025-06-06T08:54:10.000Z"
}
```

## websocket.audio\_playback\_completed

Fired when the relay has played all the backend audio it had buffered and is now idle — useful to know your backend has finished speaking.

**Payload schema**

| Field          | Type    | Description                                                                                                 |
| -------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
| `event`        | string  | Always `"websocket.audio_playback_completed"`. **Required**                                                 |
| `session_uuid` | string  | The session the relay is attached to. **Required**                                                          |
| `played_ms`    | integer | Milliseconds of backend audio played before going idle (e.g. `4200`), or `0` when unavailable. **Required** |
| `timestamp`    | string  | RFC 3339 / ISO 8601 UTC timestamp at which the event was emitted. **Required**                              |

```json
{
  "event": "websocket.audio_playback_completed",
  "session_uuid": "acW68-f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "played_ms": 4200,
  "timestamp": "2025-06-06T08:54:25.000Z"
}
```

## Dispatch acknowledgement events

The relay control commands each emit a short acknowledgement event. It confirms that the command was accepted, **not** that the audio change has taken effect. These events carry `event`, `session_uuid`, `operation_uuid`, and `timestamp`.

```json
{
  "event": "command.websocket.pause.accepted",
  "session_uuid": "acW68-f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "operation_uuid": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
  "timestamp": "2025-06-06T08:54:30.000Z"
}
```

### command.websocket.pause.accepted

Confirms dispatch of `websocket/pause`, which requests pausing audio in both directions while keeping the connection open. It stops sending user audio to the backend and stops playing backend audio to the session. Pausing does not flush buffered backend audio.

### command.websocket.resume.accepted

Confirms dispatch of `websocket/resume`, which requests resuming audio in both directions. It does not unmute either leg; mute settings applied before or during the pause remain in effect.

### command.websocket.mute.accepted

Confirms dispatch of `websocket/mute`, which requests muting a leg (`user`, `backend`, or `all`).

### command.websocket.unmute.accepted

Confirms dispatch of `websocket/unmute`, which requests unmuting a leg.

### command.websocket.flush.accepted

Confirms dispatch of `websocket/flush`, which requests discarding buffered backend audio (barge-in).

## Triggered by

| Command            | Webhooks                                                                                                                                                                     | Timing       |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| `websocket/start`  | [`websocket.connected`](#websocketconnected) or [`websocket.failed`](#websocketfailed) (then [`websocket.disconnected`](#websocketdisconnected) later when the relay closes) | Asynchronous |
| `websocket/stop`   | [`call.ended`](/webhooks/call-events#callended) for a standalone WebSocket session; no webhook for a relay attached to a phone call                                          | Immediate    |
| `websocket/pause`  | [`command.websocket.pause.accepted`](#commandwebsocketpauseaccepted)                                                                                                         | Immediate    |
| `websocket/resume` | [`command.websocket.resume.accepted`](#commandwebsocketresumeaccepted)                                                                                                       | Immediate    |
| `websocket/mute`   | [`command.websocket.mute.accepted`](#commandwebsocketmuteaccepted)                                                                                                           | Immediate    |
| `websocket/unmute` | [`command.websocket.unmute.accepted`](#commandwebsocketunmuteaccepted)                                                                                                       | Immediate    |
| `websocket/flush`  | [`command.websocket.flush.accepted`](#commandwebsocketflushaccepted)                                                                                                         | Immediate    |

[`websocket.message_received`](#websocketmessage_received) and [`websocket.audio_playback_completed`](#websocketaudio_playback_completed) are backend-driven — they fire whenever your backend sends a text frame or finishes playing its buffered audio, not in response to a command.