> ## Documentation index
> Fetch the complete documentation index at: https://developer.rhombus.com/llms.txt
> Use it to discover all available pages before exploring further.

# WebSocket Connection Lifecycle

> Manage the full lifecycle of a Rhombus WebSocket connection — perform STOMP handshakes, send heartbeats, handle reconnections, and close gracefully.

This guide covers the full lifecycle of a Rhombus WebSocket connection: establishing the connection, performing the STOMP handshake, maintaining the connection with heartbeats, and handling disconnections gracefully.

## Lifecycle overview

```text
┌─────────────────┐
│  1. WebSocket    │
│     Handshake    │──── WSS + Auth Headers
└────────┬────────┘
         ▼
┌─────────────────┐
│  2. STOMP        │
│     CONNECT      │──── Version + Heartbeat negotiation
└────────┬────────┘
         ▼
┌─────────────────┐
│  3. SUBSCRIBE    │──── Topic subscription
└────────┬────────┘
         ▼
┌─────────────────┐
│  4. MESSAGE      │◄─── Real-time events
│     Loop         │───► Heartbeat ping/pong
└────────┬────────┘
         ▼
┌─────────────────┐
│  5. DISCONNECT   │──── Graceful shutdown
└─────────────────┘
```

## Step 1: WebSocket handshake

Initiate a secure WebSocket connection to the Rhombus endpoint with your credential. This example uses an API key; OAuth access tokens and federated tokens work too (see [WebSocket authentication](https://developer.rhombus.com/websocket/authentication)):

```python
import websocket

ws = websocket.create_connection(
    "wss://ws.rhombussystems.com:8443/websocket?x-auth-scheme=api-token",
    header={"x-auth-apikey": API_TOKEN},
    timeout=10  # 10-second handshake timeout
)
```

A successful handshake upgrades the HTTP connection to WebSocket. If authentication fails, the server answers before the upgrade completes with an HTTP error and a JSON body whose `errorCode` says why: 401 when the credential is missing, unknown, expired or revoked, 403 when it's valid but not allowed (for example, a partner credential without `x-auth-org`). See [WebSocket authentication](https://developer.rhombus.com/websocket/authentication#troubleshooting).

## Step 2: STOMP CONNECT

After the WebSocket connection is established, send a STOMP `CONNECT` frame to initialize the messaging protocol:

### CONNECT frame

```text
CONNECT
accept-version:1.2
heart-beat:10000,10000

\x00
```

| Header           | Value         | Description                                            |
| ---------------- | ------------- | ------------------------------------------------------ |
| `accept-version` | `1.2`         | STOMP protocol version                                 |
| `heart-beat`     | `10000,10000` | Client send/receive heartbeat interval in milliseconds |

### Expected response: CONNECTED frame

```text
CONNECTED
version:1.2
heart-beat:10000,10000

\x00
```

The server echoes back the negotiated version and heartbeat intervals.

### Building STOMP frames

STOMP frames follow a simple text-based format:

```text
COMMAND\n
header1:value1\n
header2:value2\n
\n
body (optional)
\x00
```

- Each frame starts with a command name
- Headers are key-value pairs separated by colons, one per line
- An empty line separates headers from the body
- The frame ends with a null byte (`\x00`)

```python Python
def build_stomp_frame(command, headers=None, body=""):
    """Construct a STOMP protocol frame."""
    frame = command + "\n"
    if headers:
        for key, value in headers.items():
            frame += f"{key}:{value}\n"
    frame += "\n"
    frame += body
    frame += "\x00"
    return frame

# Send CONNECT
connect_frame = build_stomp_frame("CONNECT", {
    "accept-version": "1.2",
    "heart-beat": "10000,10000"
})
ws.send(connect_frame)

# Read CONNECTED response
response = ws.recv()
```

```javascript JavaScript
function buildStompFrame(command, headers = {}, body = "") {
  let frame = command + "\n";
  for (const [key, value] of Object.entries(headers)) {
    frame += `${key}:${value}\n`;
  }
  frame += "\n";
  frame += body;
  frame += "\x00";
  return frame;
}

// Send CONNECT
ws.send(
  buildStompFrame("CONNECT", {
    "accept-version": "1.2",
    "heart-beat": "10000,10000",
  })
);
```

```go Go
func buildStompFrame(command string, headers map[string]string, body string) string {
    var b strings.Builder
    b.WriteString(command + "\n")
    for k, v := range headers {
        b.WriteString(fmt.Sprintf("%s:%s\n", k, v))
    }
    b.WriteString("\n")
    b.WriteString(body)
    b.WriteByte(0x00)
    return b.String()
}

// Send CONNECT
connectFrame := buildStompFrame("CONNECT", map[string]string{
    "accept-version": "1.2",
    "heart-beat":     "10000,10000",
}, "")
conn.WriteMessage(websocket.TextMessage, []byte(connectFrame))
```

## Step 3: Subscribe to a topic

After the STOMP connection is established, subscribe to the organization change topic:

### SUBSCRIBE frame

```text
SUBSCRIBE
id:sub-0
destination:/topic/change/{orgUuid}

\x00
```

| Header        | Value                     | Description                              |
| ------------- | ------------------------- | ---------------------------------------- |
| `id`          | `sub-0`                   | A client-defined subscription identifier |
| `destination` | `/topic/change/{orgUuid}` | The topic to subscribe to                |

```python
subscribe_frame = build_stomp_frame("SUBSCRIBE", {
    "id": "sub-0",
    "destination": f"/topic/change/{org_uuid}"
})
ws.send(subscribe_frame)
```

> **Note**: Replace `{orgUuid}` with your organization's UUID. Retrieve it via the REST API endpoint [`POST /api/org/getOrgV2`](https://developer.rhombus.com/api-reference/org-webservice/get-organization-details-v2).

## Step 4: Message loop and heartbeats

Once subscribed, your application enters a message loop where it:

1. **Receives MESSAGE frames** containing event data
2. **Sends heartbeats** to keep the connection alive
3. **Receives heartbeats** from the server

### Heartbeat mechanism

Heartbeats are single newline characters (`\n`) sent at the negotiated interval (10 seconds). Both the client and server send heartbeats. If either side stops receiving heartbeats, the connection is considered dead.

```python
import threading
import time

def send_heartbeats(ws, stop_event):
    """Send STOMP heartbeats every 10 seconds."""
    while not stop_event.is_set():
        try:
            ws.send("\n")
        except Exception:
            break
        stop_event.wait(10)

# Start heartbeat sender in background
stop_heartbeat = threading.Event()
heartbeat_thread = threading.Thread(
    target=send_heartbeats,
    args=(ws, stop_heartbeat),
    daemon=True
)
heartbeat_thread.start()
```

### Parsing incoming frames

```python
def parse_stomp_frame(raw):
    """Parse a raw STOMP frame into command, headers, and body."""
    # Handle heartbeats (empty frames)
    raw = raw.lstrip("\n\r")
    if not raw or raw == "\x00":
        return None  # Heartbeat

    raw = raw.rstrip("\x00")
    parts = raw.split("\n\n", 1)

    lines = parts[0].split("\n")
    command = lines[0]

    headers = {}
    for line in lines[1:]:
        if ":" in line:
            key, value = line.split(":", 1)
            headers[key] = value

    body = parts[1] if len(parts) > 1 else ""
    return {"command": command, "headers": headers, "body": body}
```

### Message loop

```python
try:
    while True:
        raw = ws.recv()
        frame = parse_stomp_frame(raw)

        if frame is None:
            continue  # Heartbeat, skip

        if frame["command"] == "MESSAGE":
            payload = json.loads(frame["body"])
            handle_event(payload)

except KeyboardInterrupt:
    print("Shutting down...")
finally:
    stop_heartbeat.set()
    heartbeat_thread.join()
```

## Step 5: Graceful disconnection

Before closing the WebSocket, send a STOMP `DISCONNECT` frame:

```python
# Send DISCONNECT frame
disconnect_frame = build_stomp_frame("DISCONNECT")
ws.send(disconnect_frame)

# Close WebSocket
ws.close()
```

This signals to the server that the client is intentionally disconnecting, allowing it to clean up resources immediately rather than waiting for a heartbeat timeout.

## Delivery guarantees

The event WebSocket delivers each message **at most once**: messages aren't acknowledged, stored or replayed, so anything published while you are disconnected is lost, and there's no ordering guarantee. After every reconnect, read what you missed from the REST API (see [Build an event pipeline](https://developer.rhombus.com/solutions/event-pipeline)), and order events by their timestamps. The [event reference](https://developer.rhombus.com/websocket/event-reference#delivery) has the details.

## When the server closes the connection

| Close                                 | Cause                                                                                                                                                  | What to do                                                                                        |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| STOMP `ERROR`, then close code `1008` | The credential was deleted or revoked, or its role changed. The server re-checks the credential about once a minute, so this happens within 60 seconds | Check the credential before reconnecting; after a delete or revoke every handshake fails with 401 |
| STOMP `ERROR`, then close code `1002` | A subscription to another organization's topic, or a frame the server doesn't accept                                                                   | Fix the subscription; reconnecting with the same frames fails the same way                        |
| Close with no `ERROR` frame           | Missed heartbeats, no `CONNECT` within 60 seconds, a slow client, or a network or server restart                                                       | Reconnect with backoff                                                                            |

## Reconnection strategy

Network interruptions are inevitable. Implement automatic reconnection with backoff:

```python
import time

def connect_with_retry(api_token, org_uuid, max_retries=10):
    """Connect to Rhombus WebSocket with exponential backoff."""
    for attempt in range(max_retries):
        try:
            ws = websocket.create_connection(
                "wss://ws.rhombussystems.com:8443/websocket"
                "?x-auth-scheme=api-token",
                header={"x-auth-apikey": api_token},
                timeout=10
            )
            stomp_connect(ws)
            stomp_subscribe(ws, org_uuid)
            print("Connected successfully")
            return ws
        except Exception as e:
            delay = min(5 * (2 ** attempt), 60)
            print(f"Connection failed: {e}. Retrying in {delay}s...")
            time.sleep(delay)

    raise ConnectionError("Max retries exceeded")
```

### Full reconnection loop

```python
while True:
    ws = connect_with_retry(API_TOKEN, ORG_UUID)

    try:
        run_message_loop(ws)
    except websocket.WebSocketConnectionClosedException:
        print("Connection lost. Reconnecting...")
    except KeyboardInterrupt:
        print("User interrupted. Exiting.")
        break
    finally:
        try:
            ws.close()
        except Exception:
            pass
```

## Connection timeouts

| Timeout         | Value                 | Description                                                                                     |
| --------------- | --------------------- | ----------------------------------------------------------------------------------------------- |
| Handshake       | 10 seconds            | Maximum time to complete the WebSocket upgrade                                                  |
| Heartbeat       | 10 seconds            | Interval between keep-alive pings                                                               |
| Dead connection | \~30 seconds          | Approximate time before a missing heartbeat triggers disconnection                              |
| First frame     | 60 seconds            | Send the STOMP `CONNECT` frame within this time of opening the socket, or it's closed           |
| Slow client     | 10 seconds or 512 KiB | A client the server can't send to for 10 seconds, or that falls 512 KiB behind, is disconnected |

## Common issues

| Symptom                          | Likely Cause                                                                                                       | Solution                                                                                |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
| Connection drops every \~30s     | Heartbeats not being sent                                                                                          | Implement the heartbeat sender thread                                                   |
| `CONNECTED` frame never received | STOMP `CONNECT` frame malformed                                                                                    | Verify frame format includes null terminator `\x00`                                     |
| Messages not received            | Wrong topic or missing org UUID                                                                                    | Verify the org UUID matches your credential's organization (or the `x-auth-org` client) |
| Reconnection loop                | The credential was deleted, revoked or expired (401 on every handshake), or the organization is rate limited (429) | Check the credential with a REST call; on 429 wait `Retry-After` and back off           |
