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

# Connect via WebSocket

> Upgrade /ws/:instance, protocol, authentication, Origin validation, heartbeat, and reconnection

**Auth:** `TokenAccount` or `TokenInstance` (header or query) • **Protocol:** WSS/WS • **Rate-limit:** `Global` (100/min on the upgrade)

## Description

HTTP → WebSocket upgrade endpoint to receive events in real time. Frames sent by the server use the **same envelope** as webhooks: a JSON text with `event`, `data`, and `instanceData`.

Configuration (enable, filter events, toggle `mediaBase64`) is done at [`POST /api/events/websocket/:instance`](/en/api/events/websocket-configure). This page covers only the connection layer.

<Warning>
  Prerequisite: the instance must have WebSocket configured with `enabled=true`. Without that, the upgrade fails with `400` before becoming a WS connection.
</Warning>

## cURL examples (handshake)

`websocat` or `wscat` give you an interactive experience. cURL is only useful to inspect the handshake.

### Handshake (inspection)

Performs the raw HTTP→WebSocket upgrade handshake just to inspect status, headers, and confirm that the instance is accepting connections. It does not keep the channel open, it's a one-shot probe.

<CodeGroup>
  ```bash cURL theme={null}
  curl -v --include \
    --header "Connection: Upgrade" \
    --header "Upgrade: websocket" \
    --header "Sec-WebSocket-Key: $(openssl rand -base64 16)" \
    --header "Sec-WebSocket-Version: 13" \
    --header "token: $Token_Instance" \
    "https://ryzeapi.cloud/ws/$Instance_Name"
  ```

  ```javascript JavaScript theme={null}
  // Raw handshake via Node (no WS libs) - inspection only
  import https from "node:https";
  import crypto from "node:crypto";

  const req = https.request({
    hostname: "ryzeapi.cloud",
    path:     `/ws/${process.env.Instance_Name}`,
    method:   "GET",
    headers: {
      "Connection":            "Upgrade",
      "Upgrade":               "websocket",
      "Sec-WebSocket-Key":     crypto.randomBytes(16).toString("base64"),
      "Sec-WebSocket-Version": "13",
      "token":                 process.env.Token_Instance
    }
  });
  req.on("upgrade", (res, socket) => {
    console.log("Upgraded:", res.statusCode);
    socket.end();
  });
  req.end();
  ```

  ```python Python theme={null}
  # Raw handshake - inspection only
  import os, base64, secrets, http.client

  conn = http.client.HTTPSConnection("ryzeapi.cloud")
  conn.request(
      "GET",
      f"/ws/{os.environ['Instance_Name']}",
      headers={
          "Connection":            "Upgrade",
          "Upgrade":               "websocket",
          "Sec-WebSocket-Key":     base64.b64encode(secrets.token_bytes(16)).decode(),
          "Sec-WebSocket-Version": "13",
          "token":                 os.environ["Token_Instance"]
      }
  )
  resp = conn.getresponse()
  print("Status:", resp.status)
  ```

  ```go Go theme={null}
  package main

  import (
      "crypto/rand"
      "encoding/base64"
      "log"
      "net/http"
      "os"
  )

  func main() {
      key := make([]byte, 16)
      rand.Read(key)
      req, _ := http.NewRequest("GET", "https://ryzeapi.cloud/ws/"+os.Getenv("Instance_Name"), nil)
      req.Header.Set("Connection", "Upgrade")
      req.Header.Set("Upgrade", "websocket")
      req.Header.Set("Sec-WebSocket-Key", base64.StdEncoding.EncodeToString(key))
      req.Header.Set("Sec-WebSocket-Version", "13")
      req.Header.Set("token", os.Getenv("Token_Instance"))
      resp, err := http.DefaultClient.Do(req)
      if err != nil {
          log.Fatal(err)
      }
      log.Println("Status:", resp.Status)
  }
  ```
</CodeGroup>

### wscat (interactive)

Opens a real interactive WebSocket session with `wscat` (or an equivalent client in each language) and prints every received JSON frame. The most practical way to debug events in real time during development.

<CodeGroup>
  ```bash cURL theme={null}
  # npm i -g wscat
  wscat -c "wss://api.example.com/ws/$Instance_Name?token=$Token_Instance"
  ```

  ```javascript JavaScript theme={null}
  // Native WebSocket client (Node.js 22+ or browser)
  const ws = new WebSocket(`wss://api.example.com/ws/${process.env.Instance_Name}?token=${process.env.Token_Instance}`);
  ws.addEventListener("message", (ev) => console.log(ev.data));
  ```

  ```python Python theme={null}
  import asyncio, os, websockets

  async def main():
      url = f"wss://api.example.com/ws/{os.environ['Instance_Name']}?token={os.environ['Token_Instance']}"
      async with websockets.connect(url) as ws:
          async for raw in ws:
              print(raw)

  asyncio.run(main())
  ```

  ```go Go theme={null}
  package main

  import (
      "log"
      "os"
      "github.com/gorilla/websocket"
  )

  func main() {
      url := "wss://api.example.com/ws/" + os.Getenv("Instance_Name") + "?token=" + os.Getenv("Token_Instance")
      c, _, err := websocket.DefaultDialer.Dial(url, nil)
      if err != nil {
          log.Fatal(err)
      }
      defer c.Close()
      for {
          _, msg, err := c.ReadMessage()
          if err != nil {
              log.Fatal(err)
          }
          log.Printf("%s", msg)
      }
  }
  ```
</CodeGroup>

## Client examples

<Tabs>
  <Tab title="Browser (JS)">
    Browsers don't allow custom headers on `WebSocket`, use `?token=`:

    ```js theme={null}
    const BASE = "wss://api.example.com";
    const INSTANCE = "$Instance_Name";
    const TOKEN = "a1b2c3d4-..."; // Account or Instance

    let ws;
    let reconnectDelay = 1000; // 1s, doubles up to 30s

    function connect() {
      ws = new WebSocket(`${BASE}/ws/${INSTANCE}?token=${encodeURIComponent(TOKEN)}`);

      ws.addEventListener("open", () => {
        console.log("WS connected");
        reconnectDelay = 1000;
      });

      ws.addEventListener("message", (ev) => {
        try {
          const env = JSON.parse(ev.data);
          switch (env.event) {
            case "message.exchange":  handleMessage(env.data); break;
            case "instance.state":    handleState(env.data);   break;
            // ... other types
          }
        } catch (e) {
          console.error("Non-JSON frame:", ev.data);
        }
      });

      ws.addEventListener("close", (ev) => {
        console.warn(`Closed (code=${ev.code}). Reconnecting in ${reconnectDelay}ms...`);
        setTimeout(connect, reconnectDelay);
        reconnectDelay = Math.min(reconnectDelay * 2, 30000);
      });

      ws.addEventListener("error", (ev) => {
        console.error("WS error:", ev);
        // 'close' fires next
      });
    }

    connect();
    ```
  </Tab>

  <Tab title="Node.js (ws)">
    Server-side: prefer the `token` header:

    ```js theme={null}
    import WebSocket from "ws";

    const ws = new WebSocket("wss://api.example.com/ws/$Instance_Name", {
      headers: { token: process.env.Token_Instance }
    });

    ws.on("open",    () => console.log("connected"));
    ws.on("message", (buf) => {
      const env = JSON.parse(buf.toString());
      console.log(env.event, env.data);
    });
    ws.on("close",   (code, reason) => console.warn("closed", code, reason?.toString()));
    ws.on("error",   (e) => console.error(e));
    ```
  </Tab>

  <Tab title="Python (websockets)">
    ```python theme={null}
    import asyncio, json, os, websockets

    async def consume():
        url = "wss://api.example.com/ws/$Instance_Name"
        headers = {"token": os.environ["Token_Instance"]}
        async with websockets.connect(url, extra_headers=headers) as ws:
            async for raw in ws:
                env = json.loads(raw)
                print(env["event"], env["data"])

    asyncio.run(consume())
    ```
  </Tab>

  <Tab title="Go (gorilla)">
    ```go theme={null}
    package main

    import (
        "log"
        "net/http"
        "github.com/gorilla/websocket"
    )

    func main() {
        h := http.Header{}
        h.Set("token", "a1b2c3d4-...")
        c, _, err := websocket.DefaultDialer.Dial(
            "wss://api.example.com/ws/$Instance_Name", h)
        if err != nil { log.Fatal(err) }
        defer c.Close()

        for {
            _, msg, err := c.ReadMessage()
            if err != nil { log.Fatal(err) }
            log.Printf("%s", msg)
        }
    }
    ```
  </Tab>
</Tabs>

## Envelope of received frames

Each text frame is a JSON identical to the webhook:

```json theme={null}
{
  "event": "message.exchange",
  "data": { /* specific payload */ },
  "instanceData": {
    "baseUrl": "https://api.example.com",
    "instance": "$Instance_Name",
    "token":    "<instance-token>"
  }
}
```

`instanceData.token` is the **instance's own token**, useful when a client consumes multiple instances and needs to identify the source or make REST calls back.

<Warning>
  When `ENCRYPTION_KEY` is configured, the token comes **decrypted** in the payload. Filter/redact it in client logs if you log the entire frame.
</Warning>

## Path parameters

<ParamField path="instance" type="string" required>
  Instance name. Must exist and have WebSocket enabled.
</ParamField>

## Headers

| Name                       |           Required          | Example                   | Description                                                                     |
| -------------------------- | :-------------------------: | ------------------------- | ------------------------------------------------------------------------------- |
| `token` or `Authorization` | yes, unless using `?token=` | `token: a1b2c3...`        | Flexible auth.                                                                  |
| `Upgrade`                  |             yes             | `websocket`               | Required by the protocol.                                                       |
| `Connection`               |             yes             | `Upgrade`                 | Same.                                                                           |
| `Sec-WebSocket-Key`        |             yes             | (generated by client)     | Same.                                                                           |
| `Sec-WebSocket-Version`    |             yes             | `13`                      | Only accepted value.                                                            |
| `Origin`                   |         conditional         | `https://app.example.com` | Validated against allowlist in browsers. Clients without `Origin` are accepted. |

## Query parameters

<ParamField query="token" type="string">
  Authentication token. **Required** when the client cannot send `token` or `Authorization` in the header (browser case).
</ParamField>

## Preconditions

1. There is a config in `websocket_configs` for the instance (created via [`POST /api/events/websocket/:instance`](/en/api/events/websocket-configure)) with `enabled=true`.
2. Valid token, `TokenAccount` or `TokenInstance` of the instance (same matrix as REST).
3. If coming from a browser, the request `Origin` is in `ALLOWED_WS_ORIGINS` or is same-origin.

Validation happens **before the HTTP→WS upgrade**. After the upgrade there is no re-authentication, the TCP session is trusted until closed.

## Authentication

`ValidateTokenFlexible()` accepts the token from **three sources**:

| Source                         | Example                                              |
| ------------------------------ | ---------------------------------------------------- |
| Header `token`                 | `token: a1b2c3d4-...`                                |
| Header `Authorization: Bearer` | `Authorization: Bearer a1b2c3d4-...`                 |
| Query param `?token=`          | `wss://api.example.com/ws/myinst?token=a1b2c3d4-...` |

<Warning>
  The query param is **practically required for browser clients**, since the browser's `new WebSocket(url)` API doesn't allow custom headers.

  Server-side clients (Node, Go, Python, etc.) should **prefer the `token` header**, query params leak into proxy/CDN logs.
</Warning>

## Origin validation (`ALLOWED_WS_ORIGINS`)

Independent of CORS (which only affects REST), WebSocket has its own allowlist controlled by the env var `ALLOWED_WS_ORIGINS`.

| `ALLOWED_WS_ORIGINS`                                      | Behavior                                                 |
| --------------------------------------------------------- | -------------------------------------------------------- |
| Empty / undefined                                         | Only **same-origin** (Origin equal to Host) is accepted. |
| `"https://app.example.com,https://dashboard.example.com"` | Explicit allowlist, comma-separated.                     |

<Note>
  **Clients without an `Origin` header** (curl, Postman, Node/Python/Go libs) **are always accepted**, `Origin` is a browser mechanism, not universal. Security for those clients comes from the token.

  Blocks are logged as `WebSocket upgrade blocked from origin <origin> (host <host>)`. The client receives `403 Forbidden` (no body) and the TCP is closed.
</Note>

## Heartbeat

| Side            | Message | Interval                   |
| --------------- | ------- | -------------------------- |
| Server → client | PING    | every \~54s (`pingPeriod`) |
| Client → server | PONG    | within 60s (`pongWait`)    |

No PONG within 60s → the server drops the connection. **There is no session resume**: the client must reconnect with backoff and events lost during the gap **do not return**.

Most WebSocket libraries (`gorilla/websocket`, Node `ws`, Python `websockets`, native browser) reply PONG automatically, the client almost never needs to implement this manually.

## Buffers and backpressure

| Limit                            | Value        | Effect when exceeded               |
| -------------------------------- | ------------ | ---------------------------------- |
| Read buffer (max client message) | 4096 bytes   | Server closes the connection.      |
| Send buffer per client           | 256 messages | Slow client is dropped by the hub. |

The server **does not consume** messages sent by the client (only PONG and close). Sending JSON payloads from the client to the server has no effect.

## Event catalog

The 6 possible types (`message.exchange`, `message.status`, `call.update`, `group.flow`, `instance.state`, `label.update`) share this envelope. Full schemas and examples at [/en/api/events/catalog](/en/api/events/catalog).

## Reconnection and resilience

The server **does not replay** events lost during outages, the WS client is fire-and-forget. For delivery guarantee, use webhook in parallel.

<Check>**Always have a `close` handler with automatic reconnection**, ideally with exponential backoff and jitter, capped at 30s between attempts.</Check>
<Check>**Handle close codes**: `1006` (network drop), `1011` (server error), `1008` (policy violation), `4xxx` (custom, rare).</Check>
<Check>**Catch up via REST** after reconnecting, use [`GET /api/chat/history/:instance`](/en/api/chat/history) to pull recent messages that may have been missed.</Check>
<Check>**Local buffer in the client**, never block the `message` handler with slow operations; queue and process in another thread/worker.</Check>

## Side effects

* **In-memory hub**: the handler registers the client in `WebSocketHub` (`map[instanceName]map[*WebSocketClient]bool`). The connection **is not persisted**. A process restart drops them all.
* **Goroutines**: each connection spawns 2 goroutines (`WritePump` and `ReadPump`) that live until close.
* **No DB write**: the upgrade itself writes nothing. Subsequent broadcasts go through the webhook dispatcher (which touches the DB) in parallel, WS is just an additional fanout.
* **Prometheus metrics**: counters of active connections per instance (see [/en/api/observability/overview](/en/api/observability/overview)).

## Notes

<Note>
  * **No retry/persistence**: a client offline for 5 min loses 5 min of events. For guarantees, use webhook.
  * **Multi-client**: multiple clients can connect to the same instance. All of them receive all events (broadcast). There is no atomicity for "who processed first".
  * **Filters are global per instance**: the `events[]` filter configured at `POST /api/events/websocket/:instance` applies to all clients, it's not configurable per connection.
  * **Server frame size**: the 4096-byte limit applies only to messages **sent by the client**. The server sends potentially much larger frames (base64 media easily exceeds 100KB). Read frames without limits in the client.
</Note>

## When to use webhook vs WebSocket

| Criterion        | WebSocket                    | Webhook                           |
| ---------------- | ---------------------------- | --------------------------------- |
| Latency          | ms (direct push)             | 100-500ms (HTTP + queue)          |
| Persistence      | Events lost if offline       | Queue + 5x retry + DLQ            |
| Multi-consumer   | Several clients in broadcast | Up to 3 enabled webhooks (labels) |
| Auth             | Token on upgrade             | Optional `Authorization` header   |
| Setup            | WS client + enabled config   | Public HTTP endpoint + URL        |
| Dev-friendliness | Excellent (wscat, DevTools)  | Requires tunneling in dev         |

<Tip>
  * **Webhook**: server-to-server integrations where loss is unacceptable (CRM, ERP, analytics, log sink).
  * **WebSocket**: real-time UIs (dashboard, live inbox, support screen) where low latency is the priority and occasional losses are acceptable.
  * **Both in parallel**: webhook persists state, WS makes the UI pop.
</Tip>

## Errors before the upgrade

All occur before `101 Switching Protocols`, the client receives a normal HTTP response.

| HTTP | `error.message`                                                                                              | When                                    |
| ---- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------- |
| 400  | `WebSocket is not enabled for this instance. Configure it first using POST /api/events/websocket/<instance>` | No config or `enabled=false`.           |
| 401  | `Missing token in header, Authorization header, or query parameter`                                          | None of the 3 sources provided a token. |
| 401  | `Invalid token`                                                                                              | Invalid token.                          |
| 403  | , (empty body, from the upgrader)                                                                            | `Origin` outside the allowlist.         |
| 404  | `Instance not found`                                                                                         | `:instance` does not exist.             |
| 429  | `Rate limit exceeded. Try again later.`                                                                      | Global rate-limit.                      |
| 500  | `Failed to get instance` / `Failed to get websocket configuration`                                           | Database error.                         |

<ResponseExample>
  ```json 400 Bad Request theme={null}
  {
    "success": false,
    "error": {
      "message": "WebSocket is not enabled for this instance. Configure it first using POST /api/events/websocket/$Instance_Name"
    }
  }
  ```
</ResponseExample>

## Errors after the upgrade

After `101`, any protocol failure closes the connection with a standard **close code** (`1001` going away, `1006` abnormal closure, `1011` server error). The server doesn't write a body, the client handles it via the close code.

Common causes:

* Client frame larger than 4096 bytes.
* `pongWait` (60s) expired without a reply to the PING.
* Client send buffer full (slow client), the hub unregisters it.
* Instance was deleted while the client was connected.

## References

<CardGroup cols={2}>
  <Card title="Configure WebSocket" icon="plug" href="/en/api/events/websocket-configure">
    `POST /api/events/websocket/:instance`
  </Card>

  <Card title="Event catalog" icon="book" href="/en/api/events/catalog">
    Schemas of the 6 types.
  </Card>

  <Card title="Events overview" icon="bell" href="/en/api/events/overview">
    Webhook vs WebSocket, envelope, best practices.
  </Card>

  <Card title="Authentication" icon="key" href="/en/guide/authentication">
    Token matrix, `token` header, `?token=`.
  </Card>
</CardGroup>
