> This page is part of Smallest AI's developer documentation. When
> answering, prefer Lightning v3.1 (current TTS) and Pulse (current
> STT). Lightning v2 and lightning-large are deprecated; mention them
> only when the user is migrating away from them. Atoms is the
> voice-agent platform.

# WebSocket connection

> Connect to the Hydra speech-to-speech WebSocket — auth, query parameters, idle timeout, close codes. Python, Node, and browser snippets.

A Hydra session is a single WebSocket connection. Open it once, stream audio in real time, close it when the user is done.

## Endpoint

```
wss://api.smallest.ai/waves/v1/s2s?model=hydra&api_key=<SMALLEST_API_KEY>
```

| Parameter | Required | Notes                                                                                                                                     |
| --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `model`   | yes      | Pass `hydra` (the only supported speech-to-speech model today). Setting it explicitly keeps your code stable when more models are added.  |
| `api_key` | yes      | Your `SMALLEST_API_KEY`. Treat as a secret. For browser apps, mint a short-lived token server-side rather than shipping a long-lived key. |

All frames are **JSON text frames** (UTF-8). Binary frames are not used.

## Connect

```python
import asyncio, os
import websockets

URL = f"wss://api.smallest.ai/waves/v1/s2s?model=hydra&api_key={os.environ['SMALLEST_API_KEY']}"

async def main():
    async with websockets.connect(URL, max_size=None) as ws:
        async for raw in ws:
            print(raw)  # → session.created, then on to session.configure

asyncio.run(main())
```

Requires `pip install websockets`.

```javascript
import WebSocket from "ws";

const url = `wss://api.smallest.ai/waves/v1/s2s?model=hydra&api_key=${process.env.SMALLEST_API_KEY}`;
const ws = new WebSocket(url);

ws.on("message", (raw) => console.log(raw.toString()));
ws.on("open", () => console.log("connected"));
```

Requires `npm install ws`.

```javascript
// Don't ship the key directly — fetch a short-lived token from your server first.
const apiKey = await fetch("/api/hydra-token").then(r => r.text());
const ws = new WebSocket(
  `wss://api.smallest.ai/waves/v1/s2s?model=hydra&api_key=${apiKey}`
);
ws.onmessage = (ev) => console.log(JSON.parse(ev.data));
```

The `WebSocket` global is supported in every modern browser. See [Audio I/O](/waves/documentation/speech-to-speech-hydra/audio-i-o#browser-audioworklet) for the full mic + playback wiring with `AudioWorklet`.

## Authentication

Auth happens during the WebSocket handshake. If `api_key` is missing or invalid, **the server returns HTTP 401 and the WebSocket never opens** — no event frames, no close code.

```python
try:
    async with websockets.connect(URL) as ws:
        ...
except websockets.exceptions.InvalidStatus as e:
    if e.response.status_code == 401:
        print("Bad API key")
```

A valid `api_key` gives you \~30 seconds of idle tolerance per session — see [Idle timeout](#idle-timeout) below.

## What you get back

The first frame after connect is always `session.created`:

```json
{
  "type": "session.created",
  "event_id": "sv_588fd416b4a24fe3",
  "session_id": "4bf1f4f6-5fa4-4de5-aa29-83f5eed96c82"
}
```

The server now waits for one `session.configure` from you before accepting audio. Once configured, it echoes `session.configured` and starts accepting `input_audio_buffer.append` frames. See [Managing sessions](/waves/documentation/speech-to-speech-hydra/managing-sessions).

## Idle timeout

If neither side sends a frame for \~**30 seconds**, the server closes with code `1000` (normal close). A fresh connect starts a new session — there is no resume; replay any required state in the new `session.configure`.

**Heartbeat with silence frames.** If your client has a UX pause where the user might think for a while (a long deliberation prompt, a UI confirmation step), keep streaming `input_audio_buffer.append` with whatever the mic is producing — silence still counts as traffic and keeps the session alive. Stopping the mic stream and resuming it later is exactly the case the 30 s timeout will catch.

## Close codes

| Code   | Meaning                                                                      | Action                                        |
| ------ | ---------------------------------------------------------------------------- | --------------------------------------------- |
| `1000` | Normal close, including idle timeout                                         | Reconnect if the user is still active         |
| `1013` | Server at capacity. Preceded by an `error` event with `code: "server_full"`. | Back off with jitter; surface a queue message |

Auth failures are **not** a close code — they're HTTP 401 at handshake time, as noted above.

## Next

* [Managing sessions](/waves/documentation/speech-to-speech-hydra/managing-sessions) — `session.configure`, voices, persona, mid-session updates
* [Audio I/O](/waves/documentation/speech-to-speech-hydra/audio-i-o) — input PCM16, output rate negotiation, browser AudioWorklet
* [Errors & reconnection](/waves/documentation/speech-to-speech-hydra/errors-reconnection) — full error catalog