> ## 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.

# Conectar vía WebSocket

> Upgrade /ws/:instance, protocolo, autenticación, validación de Origin, heartbeat y reconexión

**Auth:** `TokenAccount` o `TokenInstance` (header o query) • **Protocolo:** WSS/WS • **Rate-limit:** `Global` (100/min en el upgrade)

## Descripción

Endpoint de upgrade HTTP → WebSocket para recibir eventos en tiempo real. Los frames enviados por el servidor usan el **mismo envoltorio** que los webhooks: un JSON de texto con `event`, `data` e `instanceData`.

La configuración (habilitar, filtrar eventos, alternar `mediaBase64`) se hace en [`POST /api/events/websocket/:instance`](/es/api/events/websocket-configure). Esta página cubre solo la capa de conexión.

<Warning>
  Prerrequisito: la instancia debe tener WebSocket configurado con `enabled=true`. Sin eso, el upgrade falla con `400` antes de convertirse en una conexión WS.
</Warning>

## Ejemplos cURL (handshake)

`websocat` o `wscat` te dan una experiencia interactiva. cURL solo es útil para inspeccionar el handshake.

### Handshake (inspección)

Realiza el handshake bruto de upgrade HTTP→WebSocket solo para inspeccionar status, cabeceras y confirmar que la instancia está aceptando conexiones. No mantiene el canal abierto: es una sonda one-shot.

<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 (interactivo)

Abre una sesión WebSocket interactiva real con `wscat` (o un cliente equivalente en cada lenguaje) e imprime cada frame JSON recibido. La forma más práctica de depurar eventos en tiempo real durante el desarrollo.

<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>

## Ejemplos de cliente

<Tabs>
  <Tab title="Browser (JS)">
    Los navegadores no permiten cabeceras personalizadas en `WebSocket`, usa `?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)">
    Lado servidor: prefiere la cabecera `token`:

    ```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>

## Envoltorio de los frames recibidos

Cada frame de texto es un JSON idéntico al webhook:

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

`instanceData.token` es el **token propio de la instancia**, útil cuando un cliente consume varias instancias y necesita identificar el origen o realizar llamadas REST de regreso.

<Warning>
  Cuando `ENCRYPTION_KEY` está configurado, el token viene **descifrado** en el payload. Filtra/redacta este valor en los logs del cliente si registras el frame completo.
</Warning>

## Parámetros de ruta

<ParamField path="instance" type="string" required>
  Nombre de la instancia. Debe existir y tener WebSocket habilitado.
</ParamField>

## Cabeceras

| Nombre                    |           Requerido           | Ejemplo                   | Descripción                                                                        |
| ------------------------- | :---------------------------: | ------------------------- | ---------------------------------------------------------------------------------- |
| `token` o `Authorization` | sí, salvo si se usa `?token=` | `token: a1b2c3...`        | Auth flexible.                                                                     |
| `Upgrade`                 |               sí              | `websocket`               | Requerido por el protocolo.                                                        |
| `Connection`              |               sí              | `Upgrade`                 | Idem.                                                                              |
| `Sec-WebSocket-Key`       |               sí              | (generado por el cliente) | Idem.                                                                              |
| `Sec-WebSocket-Version`   |               sí              | `13`                      | Único valor aceptado.                                                              |
| `Origin`                  |          condicional          | `https://app.example.com` | Validado contra allowlist en navegadores. Los clientes sin `Origin` son aceptados. |

## Parámetros de consulta

<ParamField query="token" type="string">
  Token de autenticación. **Requerido** cuando el cliente no puede enviar `token` o `Authorization` en la cabecera (caso del navegador).
</ParamField>

## Precondiciones

1. Existe una configuración en `websocket_configs` para la instancia (creada vía [`POST /api/events/websocket/:instance`](/es/api/events/websocket-configure)) con `enabled=true`.
2. Token válido, `TokenAccount` o `TokenInstance` de la instancia (misma matriz que REST).
3. Si proviene de un navegador, el `Origin` del request está en `ALLOWED_WS_ORIGINS` o es same-origin.

La validación ocurre **antes del upgrade HTTP→WS**. Después del upgrade no hay re-autenticación: la sesión TCP es confiable hasta que se cierra.

## Autenticación

`ValidateTokenFlexible()` acepta el token desde **tres fuentes**:

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

<Warning>
  El query param es **prácticamente requerido para clientes en navegador**, ya que la API `new WebSocket(url)` del navegador no permite cabeceras personalizadas.

  Los clientes del lado servidor (Node, Go, Python, etc.) deben **preferir la cabecera `token`**, los query params se filtran a logs de proxy/CDN.
</Warning>

## Validación de Origin (`ALLOWED_WS_ORIGINS`)

Independiente del CORS (que solo afecta al REST), el WebSocket tiene su propia allowlist controlada por la variable de entorno `ALLOWED_WS_ORIGINS`.

| `ALLOWED_WS_ORIGINS`                                      | Comportamiento                                        |
| --------------------------------------------------------- | ----------------------------------------------------- |
| Vacío / indefinido                                        | Solo se acepta **same-origin** (Origin igual a Host). |
| `"https://app.example.com,https://dashboard.example.com"` | Allowlist explícita, separada por comas.              |

<Note>
  **Los clientes sin la cabecera `Origin`** (curl, Postman, libs de Node/Python/Go) **siempre son aceptados**, `Origin` es un mecanismo del navegador, no universal. La seguridad para esos clientes proviene del token.

  Los bloqueos se registran como `WebSocket upgrade blocked from origin <origin> (host <host>)`. El cliente recibe `403 Forbidden` (sin body) y se cierra el TCP.
</Note>

## Heartbeat

| Lado               | Mensaje | Intervalo                 |
| ------------------ | ------- | ------------------------- |
| Servidor → cliente | PING    | cada \~54s (`pingPeriod`) |
| Cliente → servidor | PONG    | en 60s (`pongWait`)       |

Sin PONG en 60s → el servidor cierra la conexión. **No hay session resume**: el cliente debe reconectar con backoff y los eventos perdidos durante el gap **no regresan**.

La mayoría de las librerías WebSocket (`gorilla/websocket`, Node `ws`, Python `websockets`, navegador nativo) responden PONG automáticamente, el cliente casi nunca necesita implementar esto manualmente.

## Buffers y backpressure

| Límite                                   | Valor        | Efecto al excederse                     |
| ---------------------------------------- | ------------ | --------------------------------------- |
| Read buffer (mensaje máximo del cliente) | 4096 bytes   | El servidor cierra la conexión.         |
| Send buffer por cliente                  | 256 mensajes | Cliente lento es descartado por el hub. |

El servidor **no consume** mensajes enviados por el cliente (solo PONG y close). Enviar payloads JSON desde el cliente al servidor no tiene efecto.

## Catálogo de eventos

Los 6 tipos posibles (`message.exchange`, `message.status`, `call.update`, `group.flow`, `instance.state`, `label.update`) comparten este envoltorio. Esquemas y ejemplos completos en [/es/api/events/catalog](/es/api/events/catalog).

## Reconexión y resiliencia

El servidor **no replays** los eventos perdidos durante outages, el cliente WS es fire-and-forget. Para garantía de entrega, usa webhook en paralelo.

<Check>**Siempre ten un handler de `close` con reconexión automática**, idealmente con backoff exponencial y jitter, limitado a 30s entre intentos.</Check>
<Check>**Maneja los close codes**: `1006` (network drop), `1011` (server error), `1008` (policy violation), `4xxx` (custom, raros).</Check>
<Check>**Catch up vía REST** después de reconectar, usa [`GET /api/chat/history/:instance`](/es/api/chat/history) para traer mensajes recientes que pudieron perderse.</Check>
<Check>**Buffer local en el cliente**, nunca bloquees el handler de `message` con operaciones lentas; encola y procesa en otro thread/worker.</Check>

## Efectos colaterales

* **Hub en memoria**: el handler registra al cliente en `WebSocketHub` (`map[instanceName]map[*WebSocketClient]bool`). La conexión **no se persiste**. Un restart del proceso los descarta a todos.
* **Goroutines**: cada conexión genera 2 goroutines (`WritePump` y `ReadPump`) que viven hasta el close.
* **Sin escritura en BD**: el upgrade en sí no escribe nada. Los broadcasts subsiguientes pasan por el dispatcher de webhook (que toca la BD) en paralelo, el WS es solo un fanout adicional.
* **Métricas Prometheus**: contadores de conexiones activas por instancia (consulta [/es/api/observability/overview](/es/api/observability/overview)).

## Notas

<Note>
  * **Sin retry/persistencia**: un cliente offline por 5 min pierde 5 min de eventos. Para garantías, usa webhook.
  * **Multi-cliente**: varios clientes pueden conectarse a la misma instancia. Todos reciben todos los eventos (broadcast). No hay atomicidad para "quién procesó primero".
  * **Filtros son globales por instancia**: el filtro `events[]` configurado en `POST /api/events/websocket/:instance` se aplica a todos los clientes, no es configurable por conexión.
  * **Tamaño de frame del servidor**: el límite de 4096 bytes solo se aplica a mensajes **enviados por el cliente**. El servidor envía frames potencialmente mucho más grandes (media en base64 supera fácilmente 100KB). Lee frames sin límites en el cliente.
</Note>

## Cuándo usar webhook vs WebSocket

| Criterio         | WebSocket                      | Webhook                               |
| ---------------- | ------------------------------ | ------------------------------------- |
| Latencia         | ms (push directo)              | 100-500ms (HTTP + cola)               |
| Persistencia     | Eventos perdidos si offline    | Cola + 5x retry + DLQ                 |
| Multi-consumidor | Varios clientes en broadcast   | Hasta 3 webhooks habilitados (labels) |
| Auth             | Token en el upgrade            | Header `Authorization` opcional       |
| Setup            | Cliente WS + config habilitada | Endpoint HTTP público + URL           |
| Dev-friendliness | Excelente (wscat, DevTools)    | Requiere tunneling en dev             |

<Tip>
  * **Webhook**: integraciones server-to-server donde la pérdida es inaceptable (CRM, ERP, analytics, log sink).
  * **WebSocket**: UIs en tiempo real (dashboard, live inbox, pantalla de soporte) donde la baja latencia es prioridad y pérdidas ocasionales son aceptables.
  * **Ambos en paralelo**: webhook persiste el estado, WS hace que la UI vibre.
</Tip>

## Errores antes del upgrade

Todos ocurren antes de `101 Switching Protocols`, el cliente recibe una respuesta HTTP normal.

| HTTP | `error.message`                                                                                              | Cuándo                                         |
| ---- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- |
| 400  | `WebSocket is not enabled for this instance. Configure it first using POST /api/events/websocket/<instance>` | Sin config o `enabled=false`.                  |
| 401  | `Missing token in header, Authorization header, or query parameter`                                          | Ninguna de las 3 fuentes proporcionó un token. |
| 401  | `Invalid token`                                                                                              | Token inválido.                                |
| 403  | , (body vacío, desde el upgrader)                                                                            | `Origin` fuera de la allowlist.                |
| 404  | `Instance not found`                                                                                         | `:instance` no existe.                         |
| 429  | `Rate limit exceeded. Try again later.`                                                                      | Rate-limit global.                             |
| 500  | `Failed to get instance` / `Failed to get websocket configuration`                                           | Error de base de datos.                        |

<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>

## Errores después del upgrade

Después de `101`, cualquier falla de protocolo cierra la conexión con un **close code** estándar (`1001` going away, `1006` abnormal closure, `1011` server error). El servidor no escribe un body, el cliente lo maneja vía el close code.

Causas comunes:

* Frame del cliente mayor a 4096 bytes.
* `pongWait` (60s) expiró sin respuesta al PING.
* Send buffer del cliente lleno (cliente lento), el hub lo desregistra.
* La instancia fue eliminada mientras el cliente estaba conectado.

## Referencias

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

  <Card title="Catálogo de eventos" icon="book" href="/es/api/events/catalog">
    Esquemas de los 6 tipos.
  </Card>

  <Card title="Resumen de eventos" icon="bell" href="/es/api/events/overview">
    Webhook vs WebSocket, envoltorio, mejores prácticas.
  </Card>

  <Card title="Autenticación" icon="key" href="/es/guide/authentication">
    Matriz de tokens, header `token`, `?token=`.
  </Card>
</CardGroup>
