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
- Get WebSocket ticket via REST API
- Connect to WebSocket with ticket
- Send heartbeat every 30 seconds
- Subscribe to channels as needed
- Handle incoming events
- 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);
};
}