WebSocket Protocol

Real-time events over WebSocket

The OCVR WebSocket provides real-time updates for chat, presence, typing indicators, and notifications.

Connecting

Get a WebSocket Ticket

First, request a one-time ticket:

POST /v1/auth/ws-ticket
Authorization: Bearer sess_...

Response:

{
  "data": {
    "ticket": "wst_abc123..."
  }
}

Connect to WebSocket

Use the ticket to connect:

wss://api.ocvr.net/v1/ws?ticket=wst_abc123...

The ticket is single-use and expires after 30 seconds.

Message Format

All messages are JSON with a type field:

{
  "type": "message_type",
  "data": { ... }
}

Client → Server Messages

Heartbeat

Send periodically (every 30 seconds) to maintain connection and update presence:

{
  "type": "heartbeat"
}

Server responds with:

{
  "type": "heartbeat_ack",
  "data": {
    "server_time": 1703523600
  }
}

Subscribe to Channel

Start receiving messages for a channel:

{
  "type": "subscribe",
  "data": {
    "channel_id": "ch_abc123"
  }
}

Unsubscribe from Channel

{
  "type": "unsubscribe",
  "data": {
    "channel_id": "ch_abc123"
  }
}

Typing Indicator

Notify that user is typing:

{
  "type": "typing",
  "data": {
    "channel_id": "ch_abc123"
  }
}

Server → Client Messages

New Message

Received when a message is sent to a subscribed channel:

{
  "type": "message_create",
  "data": {
    "id": "msg_xyz789",
    "channel_id": "ch_abc123",
    "author": {
      "id": 12345,
      "display_name": "Sender"
    },
    "content": "Hello world!",
    "created_at": 1703523600,
    "attachments": []
  }
}

Message Updated

{
  "type": "message_update",
  "data": {
    "id": "msg_xyz789",
    "channel_id": "ch_abc123",
    "content": "Hello world! (edited)",
    "edited_at": 1703523700
  }
}

Message Deleted

{
  "type": "message_delete",
  "data": {
    "id": "msg_xyz789",
    "channel_id": "ch_abc123"
  }
}

Typing Started

Someone started typing:

{
  "type": "typing_start",
  "data": {
    "channel_id": "ch_abc123",
    "user_id": 12345,
    "display_name": "TyperName"
  }
}

Presence Update

Friend's presence changed:

{
  "type": "presence_update",
  "data": {
    "user_id": 12345,
    "presence": "online",
    "status_text": "Playing OCVR"
  }
}

Notification

New notification received:

{
  "type": "notification",
  "data": {
    "id": "notif_abc",
    "type": "friend_request",
    "title": "New Friend Request",
    "body": "User123 wants to be your friend",
    "created_at": 1703523600,
    "data": {
      "from_user_id": 456
    }
  }
}

Session Revoked

Current session was revoked (logout from another device):

{
  "type": "session_revoked",
  "data": {
    "reason": "logout_all"
  }
}

Client should disconnect and redirect to login.

Connection Lifecycle

  1. Get WebSocket ticket via REST API
  2. Connect to WebSocket with ticket
  3. Send heartbeat every 30 seconds
  4. Subscribe to channels as needed
  5. Handle incoming events
  6. On disconnect, reconnect with new ticket

Error Handling

If an error occurs, the server sends:

{
  "type": "error",
  "data": {
    "code": "INVALID_TICKET",
    "message": "WebSocket ticket is invalid or expired"
  }
}

Common error codes:

Code Description
INVALID_TICKET Ticket invalid or expired
NOT_SUBSCRIBED Action requires subscription
CHANNEL_NOT_FOUND Channel doesn't exist
NOT_MEMBER Not a member of channel

Example: JavaScript Client

async function connectWebSocket() {
  // Get ticket
  const res = await fetch('/v1/auth/ws-ticket', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${token}` }
  });
  const { data } = await res.json();

  // Connect
  const ws = new WebSocket(`wss://api.ocvr.net/v1/ws?ticket=${data.ticket}`);

  ws.onopen = () => {
    console.log('Connected');
    // Start heartbeat
    setInterval(() => {
      ws.send(JSON.stringify({ type: 'heartbeat' }));
    }, 30000);
  };

  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);
    switch (msg.type) {
      case 'message_create':
        handleNewMessage(msg.data);
        break;
      case 'presence_update':
        handlePresenceUpdate(msg.data);
        break;
      // ... handle other events
    }
  };

  ws.onclose = () => {
    console.log('Disconnected, reconnecting...');
    setTimeout(connectWebSocket, 1000);
  };
}
-- ---