WebSocket API Reference#
The ChatAPI WebSocket API enables real-time, bidirectional communication for instant messaging and presence updates.
Connection#
Endpoint#
ws://your-chatapi-instance.com/ws
wss://your-chatapi-instance.com/ws # for HTTPSAuthentication#
WebSocket connections require authentication via headers or query parameters:
Headers (recommended):
X-API-Key: your-tenant-api-key
X-User-Id: user-identifierQuery Parameters (fallback):
/ws?api_key=your-tenant-api-key&user_id=user-identifierSecurity Note: Prefer header-based authentication as query parameters may be logged or cached by proxies.
Connection Lifecycle#
- Connect: Client establishes WebSocket connection
- Authenticate: Server validates credentials
- Register: Connection registered for real-time events
- Sync: Server streams missed messages
- Communicate: Bidirectional message exchange
- Disconnect: Clean connection termination
Message Protocol#
All WebSocket messages use JSON format:
{
"type": "message_type",
"data": { ... }
}Client → Server Messages#
Send Message#
Send a message to a room.
{
"type": "send_message",
"room_id": "room_abc123",
"content": "Hello, world!",
"meta": {
"type": "text",
"mentions": ["user2"]
}
}Acknowledge Messages#
Mark messages as delivered.
{
"type": "ack",
"room_id": "room_abc123",
"seq": 43
}Typing Indicators#
Send typing start/stop events.
{
"type": "typing.start",
"room_id": "room_abc123"
}{
"type": "typing.stop",
"room_id": "room_abc123"
}Subscribe to Room#
Explicitly subscribe to room events (optional - automatic for room members).
{
"type": "subscribe",
"room_id": "room_abc123"
}Server → Client Messages#
Message Delivery#
New messages in subscribed rooms.
{
"type": "message",
"room_id": "room_abc123",
"message_id": "msg_def456",
"seq": 44,
"sender_id": "user1",
"content": "Hello, world!",
"meta": {
"type": "text",
"mentions": ["user2"]
},
"created_at": "2025-12-13T12:10:00Z"
}Acknowledgment Received#
Confirmation that an ACK was processed.
{
"type": "ack.received",
"room_id": "room_abc123",
"seq": 43,
"user_id": "user2"
}Presence Updates#
User online/offline status changes.
{
"type": "presence.update",
"user_id": "user2",
"status": "online"
}{
"type": "presence.update",
"user_id": "user2",
"status": "offline"
}Typing Indicators#
Other users’ typing status.
{
"type": "typing",
"room_id": "room_abc123",
"user_id": "user2",
"action": "start"
}{
"type": "typing",
"room_id": "room_abc123",
"user_id": "user2",
"action": "stop"
}Notifications#
Real-time notification delivery.
{
"type": "notification",
"notification_id": "notif_ghi789",
"topic": "order.shipped",
"payload": {
"order_id": "12345",
"tracking_number": "1Z999AA1234567890"
}
}Server Shutdown#
Graceful shutdown notification.
{
"type": "server.shutdown",
"reconnect_after_ms": 5000
}Connection Behavior#
On Connect#
- Validation: Server validates API key and user ID
- Registration: Connection registered for the user
- Presence Broadcast: Online status sent to room members
- Message Sync: Missed messages streamed in order
- Subscription: Automatic subscription to user’s rooms
On Disconnect#
- Presence Update: Offline status broadcast (after 5s grace period)
- Cleanup: Connection removed from active connections
- Persistence: User state maintained for reconnection
Reconnection#
Clients should implement exponential backoff reconnection:
let reconnectDelay = 1000; // Start with 1 second
function connect() {
const ws = new WebSocket('ws://your-chatapi.com/ws', [], {
headers: {
'X-API-Key': apiKey,
'X-User-Id': userId
}
});
ws.onclose = () => {
setTimeout(connect, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 30000); // Max 30 seconds
};
ws.onopen = () => {
reconnectDelay = 1000; // Reset delay on successful connection
};
}Message Ordering#
Messages are delivered in strict sequence order per room:
- Sequence Numbers: Each message has a unique
seqnumber - Guaranteed Order: Messages delivered in
seqorder - No Gaps: Clients may receive messages with non-consecutive seq numbers
- ACKs: Clients acknowledge the highest contiguous seq number received
Error Handling#
Connection Errors#
- Invalid Credentials: Connection closed immediately
- Rate Limiting: Connection temporarily suspended
- Server Errors: Connection closed with error message
Message Errors#
Invalid messages result in error responses:
{
"type": "error",
"code": "VALIDATION_ERROR",
"message": "Invalid message format",
"request_id": "req_123"
}Heartbeat/Ping#
WebSocket connections include automatic ping/pong:
- Ping Interval: 30 seconds
- Timeout: 60 seconds of inactivity
- Automatic: Handled by WebSocket protocol
Client Implementation Examples#
JavaScript (Native WebSocket)#
class ChatAPIClient {
constructor(apiKey, userId) {
this.apiKey = apiKey;
this.userId = userId;
this.ws = null;
this.reconnectDelay = 1000;
}
connect() {
this.ws = new WebSocket('ws://localhost:8080/ws', [], {
headers: {
'X-API-Key': this.apiKey,
'X-User-Id': this.userId
}
});
this.ws.onopen = () => {
console.log('Connected to ChatAPI');
this.reconnectDelay = 1000;
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
this.ws.onclose = () => {
console.log('Disconnected, reconnecting...');
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
sendMessage(roomId, content) {
this.send({
type: 'send_message',
room_id: roomId,
content: content
});
}
acknowledge(roomId, seq) {
this.send({
type: 'ack',
room_id: roomId,
seq: seq
});
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
handleMessage(message) {
switch (message.type) {
case 'message':
console.log('New message:', message);
// Handle new message
break;
case 'presence.update':
console.log('Presence update:', message);
// Handle presence change
break;
case 'server.shutdown':
console.log('Server shutting down, reconnecting in', message.reconnect_after_ms, 'ms');
setTimeout(() => this.connect(), message.reconnect_after_ms);
break;
}
}
}
// Usage
const client = new ChatAPIClient('your-api-key', 'user123');
client.connect();Python (websockets library)#
import asyncio
import json
import websockets
from websockets.exceptions import ConnectionClosedError
class ChatAPIClient:
def __init__(self, api_key, user_id, url="ws://localhost:8080/ws"):
self.api_key = api_key
self.user_id = user_id
self.url = url
self.websocket = None
self.reconnect_delay = 1.0
async def connect(self):
headers = {
"X-API-Key": self.api_key,
"X-User-Id": self.user_id
}
try:
self.websocket = await websockets.connect(
self.url,
extra_headers=headers
)
print("Connected to ChatAPI")
self.reconnect_delay = 1.0
# Start message handler
asyncio.create_task(self.handle_messages())
except Exception as e:
print(f"Connection failed: {e}")
await self.reconnect()
async def reconnect(self):
await asyncio.sleep(self.reconnect_delay)
self.reconnect_delay = min(self.reconnect_delay * 2, 30.0)
await self.connect()
async def handle_messages(self):
try:
async for message in self.websocket:
data = json.loads(message)
await self.handle_message(data)
except ConnectionClosedError:
print("Connection closed, reconnecting...")
await self.reconnect()
async def handle_message(self, message):
msg_type = message.get('type')
if msg_type == 'message':
print(f"New message in {message['room_id']}: {message['content']}")
elif msg_type == 'presence.update':
print(f"User {message['user_id']} is {message['status']}")
elif msg_type == 'server.shutdown':
delay = message.get('reconnect_after_ms', 5000) / 1000
print(f"Server shutting down, reconnecting in {delay}s")
await asyncio.sleep(delay)
await self.connect()
async def send_message(self, room_id, content):
message = {
"type": "send_message",
"room_id": room_id,
"content": content
}
await self.send(message)
async def acknowledge(self, room_id, seq):
message = {
"type": "ack",
"room_id": room_id,
"seq": seq
}
await self.send(message)
async def send(self, data):
if self.websocket:
await self.websocket.send(json.dumps(data))
# Usage
async def main():
client = ChatAPIClient('your-api-key', 'user123')
await client.connect()
# Keep the connection alive
while True:
await asyncio.sleep(1)
asyncio.run(main())Go (gorilla/websocket)#
package main
import (
"encoding/json"
"log"
"net/url"
"time"
"github.com/gorilla/websocket"
)
type ChatAPIClient struct {
apiKey string
userId string
serverURL string
conn *websocket.Conn
reconnectDelay time.Duration
}
func NewChatAPIClient(apiKey, userId, serverURL string) *ChatAPIClient {
return &ChatAPIClient{
apiKey: apiKey,
userId: userId,
serverURL: serverURL,
reconnectDelay: time.Second,
}
}
func (c *ChatAPIClient) connect() error {
u, err := url.Parse(c.serverURL + "/ws")
if err != nil {
return err
}
// Add query parameters for authentication
q := u.Query()
q.Set("api_key", c.apiKey)
q.Set("user_id", c.userId)
u.RawQuery = q.Encode()
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
return err
}
c.conn = conn
c.reconnectDelay = time.Second
log.Println("Connected to ChatAPI")
go c.handleMessages()
return nil
}
func (c *ChatAPIClient) handleMessages() {
defer c.conn.Close()
for {
var message map[string]interface{}
err := c.conn.ReadJSON(&message)
if err != nil {
log.Println("Read error:", err)
c.reconnect()
return
}
c.handleMessage(message)
}
}
func (c *ChatAPIClient) handleMessage(message map[string]interface{}) {
msgType, ok := message["type"].(string)
if !ok {
return
}
switch msgType {
case "message":
log.Printf("New message in %s: %s", message["room_id"], message["content"])
case "presence.update":
log.Printf("User %s is %s", message["user_id"], message["status"])
case "server.shutdown":
if delay, ok := message["reconnect_after_ms"].(float64); ok {
log.Printf("Server shutting down, reconnecting in %.0fms", delay)
time.Sleep(time.Duration(delay) * time.Millisecond)
c.connect()
}
}
}
func (c *ChatAPIClient) reconnect() {
c.conn = nil
time.Sleep(c.reconnectDelay)
c.reconnectDelay = min(c.reconnectDelay*2, 30*time.Second)
if err := c.connect(); err != nil {
log.Println("Reconnect failed:", err)
c.reconnect()
}
}
func (c *ChatAPIClient) SendMessage(roomID, content string) error {
message := map[string]interface{}{
"type": "send_message",
"room_id": roomID,
"content": content,
}
return c.send(message)
}
func (c *ChatAPIClient) Acknowledge(roomID string, seq int) error {
message := map[string]interface{}{
"type": "ack",
"room_id": roomID,
"seq": seq,
}
return c.send(message)
}
func (c *ChatAPIClient) send(data interface{}) error {
if c.conn == nil {
return errors.New("not connected")
}
return c.conn.WriteJSON(data)
}
func min(a, b time.Duration) time.Duration {
if a < b {
return a
}
return b
}
// Usage
func main() {
client := NewChatAPIClient("your-api-key", "user123", "ws://localhost:8080")
if err := client.connect(); err != nil {
log.Fatal("Failed to connect:", err)
}
// Keep the program running
select {}
}Best Practices#
Connection Management#
- Single Connection: Maintain one WebSocket connection per user
- Reconnection Logic: Implement exponential backoff
- Graceful Shutdown: Handle server shutdown messages
- Connection Pooling: Avoid multiple connections for the same user
Message Handling#
- Deduplication: Track message IDs to prevent duplicates
- Sequence Tracking: Maintain per-room sequence numbers
- ACK Batching: Send ACKs for highest contiguous sequence
- Error Recovery: Handle network interruptions gracefully
Performance#
- Message Batching: Send multiple messages in single WebSocket frame when possible
- Compression: Enable WebSocket compression for large messages
- Ping/Pong: Monitor connection health
- Resource Limits: Implement message size and rate limits
Security#
- Authentication: Always use secure WebSocket (WSS) in production
- Input Validation: Validate all incoming messages
- Rate Limiting: Respect server rate limits
- Token Refresh: Handle authentication token expiration