Tether Docs

WS /socket

How to connect to and interact with the WebSocket gateway for real-time presence updates.


WebSocket Endpoint

MethodPathDescription
WS/socketWebSocket gateway for presence

Protocol

OpcodeNameDirectionDescription
0EVENTServer → ClientPresence/event updates
1HELLOServer → ClientGreeting, includes heartbeat_interval
2INITIALIZEClient → ServerSubscribe to user IDs
3HEARTBEATBothHeartbeat ping/ack

Here is the sequence diagram for a typical connection:

Messages

HELLO Message

Sent by the server to the client upon connection.

{
  "op": 1,
  "d": {
    "heartbeat_interval": 30000
  }
}

INITIALIZE Message

Sent by the client to the server to subscribe to user IDs.

{
  "op": 2,
  "d": {
    "subscribe_to_ids": ["example_user_id_1", "example_user_id_2"]
  }
}

HEARTBEAT Message

Sent by both the client and server to keep the connection alive.

{
  "op": 3
}

EVENT Message

The WebSocket gateway sends events to subscribed clients when presence data changes, such as whenever the bot receives a presence update from Discord for a subscribed user.

PRESENCE_UPDATE
{
  "op": 0,
  "seq": 123,
  "t": "PRESENCE_UPDATE",
  "d": {
    "user_id": "example_user_id",
    "data": "..."
  }
}

The data field contains a snapshot of presence object for the user. See Presence Data Model

Note

The following limitations apply to clients connecting to the WebSocket gateway.

LimitationDescription
Frame Size LimitThe server caps inbound frame size to 1 MiB
Missed HeartbeatsThe server allows up to 3 missed heartbeats before disconnecting the client.

Message Details

The seq (Sequence) Field

Most messages sent from the server include a seq field, which is a per-connection sequence number. This number starts at 1 for each new connection and increases by 1 with every event message sent to the client.

  1. Purpose: The seq field allows clients to track the order of messages and detect if any messages are missed or received out of order.
  2. Scope: The sequence is unique to each connection and resets when a new WebSocket session is established.

Example:

{
  "op": 0,
  "seq": 5, // --> This is the 5th event message sent on this connection
  "t": "PRESENCE_UPDATE",
  "d": {
    "user_id": "123456789012345678",
    "data": {
      // PresenceData
      ...
  }
}

Subscribing to User IDs

When initializing the connection, clients must send an INITIALIZE message with a subscribe_to_ids field. This field is always an array, even if you are subscribing to a single user ID.

Example:

{
  "op": 2,
  "d": {
    "subscribe_to_ids": ["1234567890"]
  }
}

or for multiple users:

{
  "op": 2,
  "d": {
    "subscribe_to_ids": ["1234567890", "0987654321"]
  }
}

Always use an array for subscribe_to_ids, even for a single user.

Watching Multiple Users

When you subscribe to multiple user IDs using the subscribe_to_ids array, the server will send you updates for each user individually:

  • Initial State:
    After subscribing, you will receive a separate initial presence event for each user you requested. Each event contains only one user's data, identified by the user_id field in the event payload.

  • Live Updates:
    Whenever any of your subscribed users changes presence, you will receive a PRESENCE_UPDATE event for that specific user. Each update only contains data for a single user, and the user_id field tells you which user the update is for.

To keep track of the most recent presence data for all users you are watching:

Maintain a Local Map:
Create a local dictionary or map where the keys are user IDs and the values are the latest presence data for each user.

On Each Event:
When you receive an event (either the initial state or a live update), use the user_id field in the event payload to update the entry in your map for the corresponding user.

Access Latest Data:
At any time, your map will contain the most recent presence information for every user you are tracking, keyed by user_id.

Example (JavaScript):

Track presence for multiple users
// Map to store the latest presence for each user
const userPresence = {};

const ws = new WebSocket("wss://tether.eggwite.moe/socket");

let heartbeatInterval = null;

ws.onmessage = (evt) => {
	const msg = JSON.parse(evt.data);

	if (msg.op === 1) {
		// Start heartbeats
		heartbeatInterval = setInterval(
			() => ws.send(JSON.stringify({ op: 3 })),
			msg.d.heartbeat_interval
		);

		// Subscribe to multiple users
		ws.send(
			JSON.stringify({
				op: 2,
				d: { subscribe_to_ids: ["1234567890", "0987654321"] }
			})
		);
	}

	if (msg.op === 0 && msg.t === "PRESENCE_UPDATE") {
		const { user_id, data } = msg.d;
		// Update the presence for the correct user
		userPresence[user_id] = data;
		console.log(`Presence update for ${user_id}:`, data);
	}
};

ws.onclose = () => {
	if (heartbeatInterval) clearInterval(heartbeatInterval);
};
requirements.txt
pip install websocket-client
Track presence for multiple users
import json
import threading
import time
from websocket import WebSocketApp

user_presence = {}
heartbeat_interval = None

def on_message(ws_app, message):
    global heartbeat_interval
    msg = json.loads(message)

    if msg.get("op") == 1:
        # Start heartbeats (interval in ms)
        heartbeat_interval = msg["d"]["heartbeat_interval"] / 1000.0

        def heartbeat():
            while True:
                time.sleep(heartbeat_interval)
                try:
                    ws_app.send(json.dumps({"op": 3}))
                except Exception:
                    break

        threading.Thread(target=heartbeat, daemon=True).start()

        # Subscribe to multiple users
        ws_app.send(json.dumps({
            "op": 2,
            "d": {"subscribe_to_ids": ["1234567890", "0987654321"]}
        }))

    elif msg.get("op") == 0 and msg.get("t") == "PRESENCE_UPDATE":
        user_id = msg["d"]["user_id"]
        data = msg["d"]["data"]
        user_presence[user_id] = data
        print(f"Presence update for {user_id}: {data}")

def on_close(ws_app, close_status_code, close_msg):
    print("WebSocket closed")

def on_open(ws_app):
    print("WebSocket connection opened")

if __name__ == "__main__":
    ws = WebSocketApp(
        "wss://tether.eggwite.moe/socket",
        on_message=on_message,
        on_close=on_close,
        on_open=on_open
    )
    ws.run_forever()

Example: Subscribe and receive updates

websocket.js
// Minimal example: subscribe to presence updates for a user
const ws = new WebSocket("wss://tether.eggwite.moe/socket");

let heartbeatInterval = null;

ws.onmessage = (evt) => {

  const msg = JSON.parse(evt.data);

  if (msg.op === 1) {
    
    // Start heartbeats
    heartbeatInterval = setInterval(() => ws.send(JSON.stringify({ op: 3 })), msg.d.heartbeat_interval);

    // Subscribe to user
    ws.send(JSON.stringify({ op: 2, d: { subscribe_to_ids: ["1234567890"] } }));

  } else if (msg.op === 0 && msg.t === "PRESENCE_UPDATE") {

    // Output presence data
    console.log("presence", msg.d);
  }
};

// Clean up on close
ws.onclose = () => { if (heartbeatInterval) clearInterval(heartbeatInterval); };
requirements.txt
pip install websocket-client
websocket.py
import json
import threading
import time
from websocket import WebSocketApp

heartbeat_interval = None

def on_message(ws_app, message):
    global heartbeat_interval
    msg = json.loads(message)
    if msg.get("op") == 1:
        # Start heartbeats (server provides milliseconds)
        heartbeat_interval = msg["d"]["heartbeat_interval"] / 1000.0

        def hb():
            while True:
                time.sleep(heartbeat_interval)
                try:
                    ws_app.send(json.dumps({"op": 3}))
                except Exception:
                    break

        threading.Thread(target=hb, daemon=True).start()

        # Subscribe to user
        ws_app.send(json.dumps({"op": 2, "d": {"subscribe_to_ids": ["1234567890"]}}))
    elif msg.get("op") == 0 and msg.get("t") == "PRESENCE_UPDATE":
        # Output presence data
        print("presence", msg.get("d"))

def on_close(ws_app, close_status_code, close_msg):
    # Clean up if needed; heartbeat thread will exit when socket closes
    pass

def on_open(ws_app):
    pass

ws = WebSocketApp("wss://tether.eggwite.moe/socket",
                  on_message=on_message,
                  on_close=on_close,
                  on_open=on_open)
ws.run_forever()

You must send heartbeats at the interval specified in the HELLO message.

Error Codes

CodeNameDescription
4004unknown_opcodeReceived an unsupported op.
4005requires_data_objectINITIALIZE message did not include a valid payload.
4006invalid_payloadINITIALIZE message provided no IDs or empty subscriptions.

The server does not return error messages in response to invalid messages. Instead, it closes the connection with the appropriate close code.

Need Help? / Troubleshooting

If you run into issues or have questions about using the WebSocket gateway:

Discord

Join the Official Tether Discord Server

Get support and connect with the community.

Please provide as much detail as possible (error messages, steps to reproduce, etc.) when asking for help!


Last updated:

On this page